dsh-vscode-mode 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/cordis.patch.yml +7 -3
- package/lib/client.js +1646 -121
- package/lib/client.js.map +1 -1
- package/lib/index.js +1141 -106
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/ai/inline.ts +19 -2
- package/src/ai/svnTriage.ts +150 -0
- package/src/capture.ts +225 -51
- package/src/client/addToConversation.ts +203 -1
- package/src/client/pendingNav.ts +141 -0
- package/src/client/sidebar/panels/FileExplorer.ts +2 -15
- package/src/client/sidebar/panels/SvnPanel.ts +682 -72
- package/src/client/sidebar/panels/svnListBudget.ts +58 -0
- package/src/client/state/records.ts +7 -1
- package/src/client/styles/editor.css +42 -2
- package/src/client/svnStatus.ts +206 -1
- package/src/client/ui/AiSettings.ts +47 -14
- package/src/client/ui/DiffBox.ts +1 -1
- package/src/client/ui/DiffLauncher.ts +6 -4
- package/src/client/ui/EditorView.ts +32 -4
- package/src/client/ui/SvnAiPlanDialog.ts +293 -0
- package/src/client/ui/icons.ts +19 -0
- package/src/compat.ts +25 -4
- package/src/fileOpenSettings.ts +128 -26
- package/src/index.ts +7 -4
- package/src/model.ts +2 -1
- package/src/remoteWorkspace.ts +177 -0
- package/src/rpc.ts +19 -5
- package/src/shared/ai.ts +12 -0
- package/src/shared/rpc.ts +16 -1
- package/src/shared/svn.ts +296 -10
- package/src/shared/types.ts +5 -1
- package/src/svn.ts +235 -13
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — SVN 变更列表渐进渲染的预算切分(纯函数,可单测)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不虚拟滚动:工程无虚拟列表基建,且 changelist 分组头与「显示更多」尾部
|
|
5
|
+
* 天然兼容渐进展示——预算切分即可把单次渲染行数从全量(host 上限 4000)压到预算内。
|
|
6
|
+
* 为什么独立成模块:SvnPanel 顶部引 React 与 UI 原语(图标跨版本垫片),
|
|
7
|
+
* 纯函数放同文件会拖进重依赖链,单测无法轻量导入。
|
|
8
|
+
* 作者 ddj 2026年09月23号
|
|
9
|
+
*/
|
|
10
|
+
import type { SvnChangeEntry } from '../../../shared/svn.js'
|
|
11
|
+
|
|
12
|
+
/** 分组行(groupByChangelist 产出形态)。 */
|
|
13
|
+
interface SvnGroup {
|
|
14
|
+
/** changelist 名(未分组为固定文案)。 */
|
|
15
|
+
name: string
|
|
16
|
+
/** 组内条目(工作副本原顺序)。 */
|
|
17
|
+
entries: SvnChangeEntry[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 预算切分结果:预算内组透传原对象,被截断组带 full(全量计数)、折叠组带 folded 标记供组头展示。 */
|
|
21
|
+
export interface SvnBudget {
|
|
22
|
+
groups: Array<SvnGroup & { full?: number; folded?: boolean }>
|
|
23
|
+
/** 预算内条数(本次实际渲染行数)。 */
|
|
24
|
+
shown: number
|
|
25
|
+
/** 全量条数。 */
|
|
26
|
+
total: number
|
|
27
|
+
/** 是否还有未渲染行。 */
|
|
28
|
+
hasMore: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 按 DOM 预算切分分组行(渐进渲染)。
|
|
33
|
+
*
|
|
34
|
+
* 预算跨组依序分配:预算内组整组保留(透传原对象,引用稳定利于行 memo),
|
|
35
|
+
* 最后触及预算的组被截断,预算耗尽后不再产出**需要行数**的组;
|
|
36
|
+
* 零行组(如折叠映射的空 entries 组)不占预算,恒产出组头。
|
|
37
|
+
* 组头计数恒显全量(full),行只渲染预算内(entries),避免「截断组计数变小」的误导。
|
|
38
|
+
* @author ddj 2026年09月23号
|
|
39
|
+
* @param groups groupByChangelist 产出的分组
|
|
40
|
+
* @param budget 允许渲染的条目行数上限
|
|
41
|
+
* @returns 截断后的分组与计数
|
|
42
|
+
*/
|
|
43
|
+
export function budgetGroupsOf(groups: readonly SvnGroup[], budget: number): SvnBudget {
|
|
44
|
+
const total = groups.reduce((sum, group) => sum + group.entries.length, 0)
|
|
45
|
+
const limit = Math.max(0, budget)
|
|
46
|
+
if (total <= limit) return { groups: [...groups], shown: total, total, hasMore: false }
|
|
47
|
+
const sliced: Array<SvnGroup & { full?: number; folded?: boolean }> = []
|
|
48
|
+
let used = 0
|
|
49
|
+
for (const group of groups) {
|
|
50
|
+
if (used >= limit && group.entries.length > 0) break
|
|
51
|
+
const take = Math.min(group.entries.length, limit - used)
|
|
52
|
+
sliced.push(take >= group.entries.length
|
|
53
|
+
? group
|
|
54
|
+
: { name: group.name, entries: group.entries.slice(0, take), full: group.entries.length })
|
|
55
|
+
used += take
|
|
56
|
+
}
|
|
57
|
+
return { groups: sliced, shown: used, total, hasMore: true }
|
|
58
|
+
}
|
|
@@ -12,7 +12,10 @@ export type Status = 'pending' | 'accepted' | 'rejected'
|
|
|
12
12
|
|
|
13
13
|
/** 记录视图摘要(按文件分组)。 */
|
|
14
14
|
export interface FileSummary {
|
|
15
|
+
/** 功能路径(分组/打开口径,恒为记录 path)。 */
|
|
15
16
|
path: string
|
|
17
|
+
/** 展示标签:远程工作区为远端路径,其余等于 path(issue #7)。 */
|
|
18
|
+
label: string
|
|
16
19
|
recs: RecordView[]
|
|
17
20
|
pending: number
|
|
18
21
|
}
|
|
@@ -86,7 +89,10 @@ export function summarize(records: RecordView[]): Summary {
|
|
|
86
89
|
byPath.get(r.path)!.push(r)
|
|
87
90
|
}
|
|
88
91
|
const files: FileSummary[] = []
|
|
89
|
-
for (const [path, recs] of byPath)
|
|
92
|
+
for (const [path, recs] of byPath) {
|
|
93
|
+
const label = recs.find((r) => !!r.displayPath)?.displayPath ?? path
|
|
94
|
+
files.push({ path, label, recs, pending: pendingCount(recs) })
|
|
95
|
+
}
|
|
90
96
|
files.sort((a, b) => (a.path < b.path ? -1 : 1))
|
|
91
97
|
const pendingFiles = files.filter((f) => f.pending > 0)
|
|
92
98
|
return { files, pendingFiles, totalFiles: pendingFiles.length }
|
|
@@ -301,10 +301,25 @@
|
|
|
301
301
|
[data-edrv-view] .edrv-svn-check input { margin: 0; cursor: pointer; }
|
|
302
302
|
[data-edrv-view] .edrv-svn-bulk { display: flex; align-items: center; gap: 6px; padding: 0 10px 6px 12px; flex-shrink: 0; }
|
|
303
303
|
[data-edrv-view] .edrv-svn-count { font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
304
|
+
/* AI 助手 dock(12-panel-ai-bar 设计定稿):manifest 分带之 assistant 带,accent 左竖轨为唯一记忆点。
|
|
305
|
+
AI 线程色三处同轨:竖轨 / dock 按钮描边 / 弹窗进度条(--dsw-alias-state-info-primary)。 @author ddj 2026-09-23 */
|
|
306
|
+
[data-edrv-view] .edrv-svn-ai-bar { position: relative; display: flex; align-items: center; gap: 8px; padding: 6px 10px 6px 14px; background: var(--dsw-alias-bg-layer-2, #f7f9f9); border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); flex-shrink: 0; }
|
|
307
|
+
[data-edrv-view] .edrv-svn-ai-rail { position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--dsw-alias-state-info-primary, #2f6fed); }
|
|
308
|
+
[data-edrv-view] .edrv-svn-ai-label { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary, #1f2933); white-space: nowrap; }
|
|
309
|
+
[data-edrv-view] .edrv-svn-ai-btn { border-color: var(--dsw-alias-state-info-primary, #2f6fed); color: var(--dsw-alias-state-info-primary, #2f6fed); }
|
|
310
|
+
[data-edrv-view] .edrv-svn-ai-btn:disabled { border-color: var(--dsw-alias-border-l1, #e0e6e8); color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
311
|
+
/* 状态位定宽右对齐(就绪/分析中…/等待助手投递…互换不跳版) */
|
|
312
|
+
[data-edrv-view] .edrv-svn-ai-status { flex-shrink: 0; min-width: 88px; text-align: right; font-size: 10px; color: var(--dsw-alias-label-tertiary, #9aa5b1); white-space: nowrap; }
|
|
313
|
+
/* 批量条语义分隔(导出簇 ┊ 动作簇之间 1px 竖线) */
|
|
314
|
+
[data-edrv-view] .edrv-svn-sep { width: 1px; height: 14px; background: var(--dsw-alias-border-l1, #e0e6e8); flex-shrink: 0; }
|
|
315
|
+
/* 头部图标组(检查远端/刷新成组,gap 收紧) */
|
|
316
|
+
[data-edrv-view] .edrv-svn-head-icons { display: inline-flex; align-items: center; gap: 2px; flex-shrink: 0; }
|
|
304
317
|
[data-edrv-view] .edrv-svn-list { flex: 1; min-height: 0; overflow: auto; padding: 2px 6px 12px; font-size: 12px; }
|
|
305
|
-
[data-edrv-view] .edrv-svn-group { display: flex; align-items: center; gap: 6px; height: 22px; padding: 0 6px; margin: 2px 0; border-radius: 5px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-secondary, #52606d); font-size: 10px; font-weight: 600; }
|
|
318
|
+
[data-edrv-view] .edrv-svn-group { display: flex; align-items: center; gap: 6px; height: 22px; padding: 0 6px; margin: 2px 0; border-radius: 5px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-secondary, #52606d); font-size: 10px; font-weight: 600; cursor: pointer; user-select: none; }
|
|
319
|
+
[data-edrv-view] .edrv-svn-group:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
320
|
+
[data-edrv-view] .edrv-svn-fold-ch { width: 10px; flex-shrink: 0; text-align: center; color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
306
321
|
[data-edrv-view] .edrv-svn-group-n { margin-left: auto; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-weight: 400; }
|
|
307
|
-
[data-edrv-view] .edrv-svn-row { display: flex; align-items: center; gap: 6px; height: 22px; padding: 0 6px; border-radius: 5px; cursor:
|
|
322
|
+
[data-edrv-view] .edrv-svn-row { display: flex; align-items: center; gap: 6px; height: 22px; padding: 0 6px; border-radius: 5px; cursor: pointer; white-space: nowrap; }
|
|
308
323
|
[data-edrv-view] .edrv-svn-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
309
324
|
[data-edrv-view] .edrv-svn-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-primary, #1f2933); }
|
|
310
325
|
[data-edrv-view] .edrv-svn-acts { display: none; align-items: center; gap: 4px; flex-shrink: 0; }
|
|
@@ -367,6 +382,31 @@
|
|
|
367
382
|
[data-edrv-view] .edrv-svn-patch-warn { color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; }
|
|
368
383
|
[data-edrv-view] .edrv-svn-patch-pre { max-height: 46vh; overflow: auto; margin: 0 0 8px; padding: 8px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; background: var(--dsw-alias-bg-layer-2, #f7f9f9); color: var(--dsw-alias-label-primary, #1f2933); font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 11px; line-height: 1.5; white-space: pre; user-select: text; }
|
|
369
384
|
[data-edrv-view] .edrv-svn-patch-foot { display: flex; align-items: center; gap: 8px; }
|
|
385
|
+
/* AI 智能整理弹窗(11-ai-changelist-triage):预览三分组卡 + 执行进度 @author ddj 2026-09-23 */
|
|
386
|
+
[data-edrv-view] .edrv-svnplan-dialog { max-width: calc(100vw - 48px); }
|
|
387
|
+
[data-edrv-view] .edrv-svnplan-head { display: flex; align-items: center; gap: 8px; padding-bottom: 4px; }
|
|
388
|
+
[data-edrv-view] .edrv-svnplan-meta { display: flex; gap: 10px; padding-bottom: 6px; }
|
|
389
|
+
[data-edrv-view] .edrv-svnplan-note { color: var(--dsw-alias-state-warn-primary, #b7791f); font-size: 10px; }
|
|
390
|
+
[data-edrv-view] .edrv-svnplan-sec { border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; padding: 6px 8px; margin-bottom: 8px; }
|
|
391
|
+
[data-edrv-view] .edrv-svnplan-sechead { display: flex; align-items: center; gap: 6px; }
|
|
392
|
+
[data-edrv-view] .edrv-svnplan-sectitle { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary, #1f2933); white-space: nowrap; }
|
|
393
|
+
[data-edrv-view] .edrv-svnplan-list { max-height: 26vh; overflow: auto; padding-left: 18px; }
|
|
394
|
+
[data-edrv-view] .edrv-svnplan-card { border-top: 1px solid var(--dsw-alias-border-l1, #e0e6e8); padding: 3px 0; }
|
|
395
|
+
[data-edrv-view] .edrv-svnplan-row { display: flex; align-items: center; gap: 6px; }
|
|
396
|
+
[data-edrv-view] .edrv-svnplan-item { display: flex; align-items: center; gap: 6px; padding-left: 18px; }
|
|
397
|
+
[data-edrv-view] .edrv-svnplan-groupname { font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-primary, #1f2933); cursor: pointer; white-space: nowrap; }
|
|
398
|
+
[data-edrv-view] .edrv-svnplan-path { font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 11px; color: var(--dsw-alias-label-secondary, #52606d); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 46ch; }
|
|
399
|
+
[data-edrv-view] .edrv-svnplan-reason { font-size: 10px; color: var(--dsw-alias-label-tertiary, #9aa5b1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 30ch; }
|
|
400
|
+
[data-edrv-view] .edrv-svnplan-stage { display: flex; align-items: center; gap: 8px; padding-bottom: 6px; }
|
|
401
|
+
[data-edrv-view] .edrv-svnplan-badge { border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 5px; padding: 1px 8px; font-size: 10px; color: var(--dsw-alias-label-secondary, #52606d); background: var(--dsw-alias-bg-layer-2, #f7f9f9); white-space: nowrap; }
|
|
402
|
+
[data-edrv-view] .edrv-svnplan-badge-cur { font-family: var(--ds-font-family-code, ui-monospace, monospace); overflow: hidden; text-overflow: ellipsis; }
|
|
403
|
+
[data-edrv-view] .edrv-svnplan-bar { height: 6px; border-radius: 3px; background: var(--dsw-alias-bg-layer-2, #f7f9f9); border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); overflow: hidden; margin-bottom: 8px; }
|
|
404
|
+
[data-edrv-view] .edrv-svnplan-bar-in { height: 100%; background: var(--dsw-alias-state-info-primary, #2f6fed); transition: width .2s; }
|
|
405
|
+
[data-edrv-view] .edrv-svnplan-logwrap { max-height: 40vh; overflow: auto; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; padding: 6px 8px; background: var(--dsw-alias-bg-layer-2, #f7f9f9); }
|
|
406
|
+
[data-edrv-view] .edrv-svnplan-log { font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 11px; line-height: 1.6; color: var(--dsw-alias-label-secondary, #52606d); user-select: text; }
|
|
407
|
+
[data-edrv-view] .edrv-svnplan-log-bad { color: var(--dsw-alias-state-error-primary, #d9534f); }
|
|
408
|
+
[data-edrv-view] .edrv-svnplan-foot { display: flex; align-items: center; gap: 8px; }
|
|
409
|
+
[data-edrv-view] .edrv-svnplan-primary { font-weight: 600; border-color: var(--dsw-alias-state-info-primary, #2f6fed); }
|
|
370
410
|
[data-edrv-view] .edrv-svn-sum-form { display: flex; align-items: center; gap: 10px; padding-bottom: 8px; }
|
|
371
411
|
[data-edrv-view] .edrv-svn-sum-list { max-height: 44vh; overflow: auto; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; padding: 2px; margin-bottom: 8px; }
|
|
372
412
|
[data-edrv-view] .edrv-svnlog-path-kind { flex-shrink: 0; padding: 0 5px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 10px; }
|
package/src/client/svnStatus.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 动作:svnUpdate / svnTortoise / svnRevert / svnAdd / svnDiffBase 统一执行与反馈文案。
|
|
9
9
|
* 作者 ddj 2026年09月16号
|
|
10
10
|
*/
|
|
11
|
-
import type { SvnAction, SvnChangeEntry, SvnItemStatus, SvnStatusPayload, SvnUpdateEntry } from '../shared/svn.js'
|
|
11
|
+
import type { SvnAction, SvnAiPlan, SvnChangeEntry, SvnIgnoreItem, SvnItemStatus, SvnStatusPayload, SvnUpdateEntry } from '../shared/svn.js'
|
|
12
12
|
import { SVN_TORTOISE_LABELS, SVN_UPDATE_LABEL } from '../shared/svn.js'
|
|
13
13
|
import { svnActionOn, svnActionsFor } from '../shared/svnActions.js'
|
|
14
14
|
import type { SvnActionContext } from '../shared/svnActions.js'
|
|
@@ -274,6 +274,211 @@ export async function svnAdd(sessionId: string | undefined, paths: string[]): Pr
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/**
|
|
278
|
+
* 分区(changelist)批量操作:关联或解除工作副本条目的 changelist 登记。
|
|
279
|
+
* `svn changelist` 成功时输出静默(host count 为 0),反馈计数以客户端 paths.length 为准;
|
|
280
|
+
* 上限分块由面板层(svnChunksOf)负责,本包装单次直发。
|
|
281
|
+
* @author ddj 2026年09月23号
|
|
282
|
+
* @param sessionId 会话 id
|
|
283
|
+
* @param paths 工作区相对路径列表
|
|
284
|
+
* @param name 目标分区名;null = 移出当前分区
|
|
285
|
+
* @returns 执行结果与反馈文案
|
|
286
|
+
*/
|
|
287
|
+
export async function svnChangelist(
|
|
288
|
+
sessionId: string | undefined,
|
|
289
|
+
paths: string[],
|
|
290
|
+
name: string | null,
|
|
291
|
+
): Promise<SvnActionOutcome> {
|
|
292
|
+
const count = paths.length
|
|
293
|
+
const remove = name === null
|
|
294
|
+
try {
|
|
295
|
+
const res = await rpc('svn.changelist', remove ? { sessionId, paths, remove: true } : { sessionId, paths, name: name ?? undefined })
|
|
296
|
+
if (!res.ok) return { ok: false, message: '分区操作失败:' + res.error }
|
|
297
|
+
return {
|
|
298
|
+
ok: true,
|
|
299
|
+
message: remove ? '已移出分区(' + count + ' 项)' : '已移入分区「' + name + '」(' + count + ' 项)',
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return { ok: false, message: '分区操作失败:' + String(error) }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// --region AI 智能整理(11-ai-changelist-triage)
|
|
307
|
+
|
|
308
|
+
/** AI 分析结果(plan + 元信息;失败 message 已含前缀可直接 notify)。 */
|
|
309
|
+
export interface SvnAiPlanOutcome {
|
|
310
|
+
ok: boolean
|
|
311
|
+
/** 三段方案(ok=true 时存在)。 */
|
|
312
|
+
plan?: SvnAiPlan
|
|
313
|
+
/** 分析所依据的变更条目总数。 */
|
|
314
|
+
entriesCount?: number
|
|
315
|
+
/** 是否附带 diff 上下文(false = paths-only 降级)。 */
|
|
316
|
+
diffIncluded?: boolean
|
|
317
|
+
/** AI 被丢弃的候选路径数(幻觉/状态不符/互斥落选)。 */
|
|
318
|
+
dropped?: number
|
|
319
|
+
/** 实际使用的模型标识。 */
|
|
320
|
+
model?: string
|
|
321
|
+
/** 反馈文案(失败含原因)。 */
|
|
322
|
+
message: string
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* AI 智能整理分析(host `svn.aiPlan`;只读,LLM 一次性调用最长约 120s)。
|
|
327
|
+
* @author ddj 2026年09月23号
|
|
328
|
+
* @param sessionId 会话 id
|
|
329
|
+
* @returns 三段方案与分析元信息
|
|
330
|
+
*/
|
|
331
|
+
export async function svnAiPlan(sessionId: string | undefined): Promise<SvnAiPlanOutcome> {
|
|
332
|
+
try {
|
|
333
|
+
const res = await rpc('svn.aiPlan', { sessionId })
|
|
334
|
+
if (!res.ok) return { ok: false, message: 'AI 分析失败:' + res.error }
|
|
335
|
+
return {
|
|
336
|
+
ok: true,
|
|
337
|
+
plan: res.plan,
|
|
338
|
+
entriesCount: res.entriesCount,
|
|
339
|
+
diffIncluded: res.diffIncluded,
|
|
340
|
+
dropped: res.dropped,
|
|
341
|
+
model: res.model,
|
|
342
|
+
message: 'AI 分析完成',
|
|
343
|
+
}
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return { ok: false, message: 'AI 分析失败:' + String(error) }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* AI 整理执行段③:按目录合并写入 svn:ignore 属性(host `svn.ignore`)。
|
|
351
|
+
* @author ddj 2026年09月23号
|
|
352
|
+
* @param sessionId 会话 id
|
|
353
|
+
* @param items 目录聚合写入项
|
|
354
|
+
* @returns 执行结果与反馈文案
|
|
355
|
+
*/
|
|
356
|
+
export async function svnIgnore(sessionId: string | undefined, items: SvnIgnoreItem[]): Promise<SvnActionOutcome> {
|
|
357
|
+
try {
|
|
358
|
+
const res = await rpc('svn.ignore', { sessionId, items })
|
|
359
|
+
if (!res.ok) return { ok: false, message: 'SVN 忽略失败:' + res.error }
|
|
360
|
+
return { ok: true, message: res.summary }
|
|
361
|
+
} catch (error) {
|
|
362
|
+
return { ok: false, message: 'SVN 忽略失败:' + String(error) }
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** 混合通道取件结果(plan 为空 = 尚无新投递)。 */
|
|
367
|
+
export interface SvnAiPendingOutcome {
|
|
368
|
+
ok: boolean
|
|
369
|
+
/** 收件箱方案(无新件为 null)。 */
|
|
370
|
+
plan?: SvnAiPlan | null
|
|
371
|
+
/** 幻觉/落选丢弃数。 */
|
|
372
|
+
dropped?: number
|
|
373
|
+
/** 投递时刻时间戳。 */
|
|
374
|
+
at?: number
|
|
375
|
+
/** 反馈文案(失败含原因)。 */
|
|
376
|
+
message: string
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 混合通道取件(host `svn.aiPlanPending`):轮询取会话 agent 投递的方案。
|
|
381
|
+
* @author ddj 2026年09月23号
|
|
382
|
+
* @param sessionId 会话 id
|
|
383
|
+
* @param since 注入时刻时间戳(早于它的旧投递不算新件)
|
|
384
|
+
* @returns 取件结果
|
|
385
|
+
*/
|
|
386
|
+
export async function svnAiPlanPending(sessionId: string | undefined, since: number): Promise<SvnAiPendingOutcome> {
|
|
387
|
+
try {
|
|
388
|
+
const res = await rpc('svn.aiPlanPending', { sessionId, since })
|
|
389
|
+
if (!res.ok) return { ok: false, message: '取件失败:' + res.error }
|
|
390
|
+
return { ok: true, plan: res.plan, dropped: res.dropped, at: res.at, message: res.plan ? '方案已送达' : '暂无方案' }
|
|
391
|
+
} catch (error) {
|
|
392
|
+
return { ok: false, message: '取件失败:' + String(error) }
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** 执行引擎单步(label = 进度/日志文案;kind/n 供汇总分段计数)。 */
|
|
397
|
+
export interface SvnPlanStep {
|
|
398
|
+
/** 步骤展示文案。 */
|
|
399
|
+
label: string
|
|
400
|
+
/** 汇总分类(还原/分组/忽略)。 */
|
|
401
|
+
kind: 'revert' | 'group' | 'ignore'
|
|
402
|
+
/** 本步动作条目数(汇总计数用)。 */
|
|
403
|
+
n: number
|
|
404
|
+
/** 汇总去重键(kind='group' 时为组名;跨块同组只计一次)。 */
|
|
405
|
+
key?: string
|
|
406
|
+
/** 步骤执行(RPC 封装)。 */
|
|
407
|
+
run: () => Promise<SvnActionOutcome>
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** 执行引擎控制面(paused/cancelled 标志 + 步骤边界等待)。 */
|
|
411
|
+
export interface SvnPlanCtl {
|
|
412
|
+
/** 是否请求暂停(步骤边界生效)。 */
|
|
413
|
+
paused: () => boolean
|
|
414
|
+
/** 是否请求取消剩余。 */
|
|
415
|
+
cancelled: () => boolean
|
|
416
|
+
/** 暂停点等待(paused 且未 cancelled 时挂起,直至外部恢复/取消)。 */
|
|
417
|
+
wait: () => Promise<void>
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** 执行引擎进度快照(每次步骤前后回调,驱动 UI 重渲)。 */
|
|
421
|
+
export interface SvnPlanTick {
|
|
422
|
+
/** 已完成步数。 */
|
|
423
|
+
done: number
|
|
424
|
+
/** 总步数。 */
|
|
425
|
+
total: number
|
|
426
|
+
/** 当前步骤文案(done 步之后的下一步;末尾为空串)。 */
|
|
427
|
+
cur: string
|
|
428
|
+
/** 逐步日志(✓/✗ + 文案)。 */
|
|
429
|
+
log: Array<{ ok: boolean; text: string }>
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** 执行引擎结果(逐步日志 + 是否被取消)。 */
|
|
433
|
+
export interface SvnPlanResult {
|
|
434
|
+
log: Array<{ ok: boolean; text: string }>
|
|
435
|
+
cancelled: boolean
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* 可暂停执行引擎(AI 整理执行段编排):顺序执行步骤队列。
|
|
440
|
+
* - 暂停只在**步骤边界**生效(svn 子进程不可安全中断):每步前 `ctl.wait()`;
|
|
441
|
+
* - 取消后不再取步(已执行不回滚);
|
|
442
|
+
* - 单步失败(ok=false 或抛错)记日志后**继续**后续步骤。
|
|
443
|
+
* @author ddj 2026年09月23号
|
|
444
|
+
* @param steps 步骤队列(执行顺序即数组顺序)
|
|
445
|
+
* @param ctl 暂停/取消控制面
|
|
446
|
+
* @param onTick 进度回调(步骤前后各一次)
|
|
447
|
+
* @returns 逐步日志与取消标记
|
|
448
|
+
*/
|
|
449
|
+
export async function runPlanSteps(
|
|
450
|
+
steps: readonly SvnPlanStep[],
|
|
451
|
+
ctl: SvnPlanCtl,
|
|
452
|
+
onTick?: (tick: SvnPlanTick) => void,
|
|
453
|
+
): Promise<SvnPlanResult> {
|
|
454
|
+
const log: Array<{ ok: boolean; text: string }> = []
|
|
455
|
+
const tick = (done: number) => onTick?.({
|
|
456
|
+
done,
|
|
457
|
+
total: steps.length,
|
|
458
|
+
cur: done < steps.length ? steps[done].label : '',
|
|
459
|
+
log: [...log],
|
|
460
|
+
})
|
|
461
|
+
let cancelled = false
|
|
462
|
+
tick(0)
|
|
463
|
+
for (let index = 0; index < steps.length; index++) {
|
|
464
|
+
if (ctl.cancelled()) { cancelled = true; break }
|
|
465
|
+
await ctl.wait()
|
|
466
|
+
if (ctl.cancelled()) { cancelled = true; break }
|
|
467
|
+
const step = steps[index]
|
|
468
|
+
let outcome: SvnActionOutcome
|
|
469
|
+
try {
|
|
470
|
+
outcome = await step.run()
|
|
471
|
+
} catch (error) {
|
|
472
|
+
outcome = { ok: false, message: String(error) }
|
|
473
|
+
}
|
|
474
|
+
log.push({ ok: outcome.ok, text: step.label + ':' + outcome.message })
|
|
475
|
+
tick(index + 1)
|
|
476
|
+
}
|
|
477
|
+
return { log, cancelled }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// --endregion
|
|
481
|
+
|
|
277
482
|
// --region W2 差异管理补全(补丁/目录对比/远端检查/冲突/配对)
|
|
278
483
|
|
|
279
484
|
/**
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* 开关 + 模型下拉(edrv.ai.models 目录,按 provider 分组,默认「自动」)+
|
|
5
5
|
* 思考强度下拉(随所选模型的 efforts 元数据动态渲染:无档位隐藏;含「跟随默认」)。
|
|
6
6
|
* 保存走 edrv.ai.configUpdate;保存后广播 edrv:ai-config 事件让 provider 即时生效。
|
|
7
|
+
* 第二组「AI 任务模型」:非补全 AI 调用(AI 智能整理等)专用路由,独立于补全模型;
|
|
8
|
+
* 留空 = 跟随补全模型配置(向后兼容)。
|
|
7
9
|
* 作者 ddj
|
|
8
10
|
*/
|
|
9
11
|
import React from 'react'
|
|
@@ -25,8 +27,12 @@ export function AiSettings() {
|
|
|
25
27
|
React.useEffect(() => {
|
|
26
28
|
Promise.all([rpc('edrv.ai.configGet', {}), rpc('edrv.ai.models', {})])
|
|
27
29
|
.then(([c, d]) => {
|
|
28
|
-
if (c.ok)
|
|
29
|
-
|
|
30
|
+
if (c.ok) {
|
|
31
|
+
setCfg({
|
|
32
|
+
enabled: c.enabled, provider: c.provider, model: c.model, effort: c.effort,
|
|
33
|
+
taskProvider: c.taskProvider || '', taskModel: c.taskModel || '', taskEffort: c.taskEffort || '',
|
|
34
|
+
})
|
|
35
|
+
} else setError(c.error || '配置读取失败')
|
|
30
36
|
if (d.ok) setDir(d)
|
|
31
37
|
else setError(d.error || '模型目录读取失败')
|
|
32
38
|
})
|
|
@@ -40,11 +46,11 @@ export function AiSettings() {
|
|
|
40
46
|
return dir.providers.find((p) => p.id === provider)?.models ?? []
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
/**
|
|
44
|
-
const
|
|
45
|
-
const models = modelsOf(
|
|
46
|
-
return models.find((m) => m.model ===
|
|
47
|
-
}
|
|
49
|
+
/** 档位可选项(补全/任务两组选择器共用):模型无 reasoning 元数据 → 空数组(隐藏)。 */
|
|
50
|
+
const effortsOf = (provider, model) => {
|
|
51
|
+
const models = modelsOf(provider)
|
|
52
|
+
return models.find((m) => m.model === model)?.efforts ?? models[0]?.efforts ?? []
|
|
53
|
+
}
|
|
48
54
|
|
|
49
55
|
const save = async (patch) => {
|
|
50
56
|
setBusy(true)
|
|
@@ -52,7 +58,10 @@ export function AiSettings() {
|
|
|
52
58
|
try {
|
|
53
59
|
const res = await rpc('edrv.ai.configUpdate', patch)
|
|
54
60
|
if (!res.ok) throw new Error(res.error || '保存失败')
|
|
55
|
-
setCfg({
|
|
61
|
+
setCfg({
|
|
62
|
+
enabled: res.enabled, provider: res.provider, model: res.model, effort: res.effort,
|
|
63
|
+
taskProvider: res.taskProvider || '', taskModel: res.taskModel || '', taskEffort: res.taskEffort || '',
|
|
64
|
+
})
|
|
56
65
|
setAiInlineEnabled(res.enabled)
|
|
57
66
|
setNote('已保存')
|
|
58
67
|
window.dispatchEvent(new CustomEvent('edrv:ai-config', { detail: { enabled: res.enabled } }))
|
|
@@ -65,24 +74,30 @@ export function AiSettings() {
|
|
|
65
74
|
|
|
66
75
|
const toggle = () => save({ enabled: !cfg.enabled })
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!value) return
|
|
77
|
+
/** 下拉值 `provider/model` → 换模型 patch(档位不属新模型时一并清空;补全/任务两组共用)。 */
|
|
78
|
+
const patchOfModel = (value, keys) => {
|
|
79
|
+
if (!value) return { [keys.provider]: '', [keys.model]: '' }
|
|
71
80
|
const idx = value.indexOf('/')
|
|
72
81
|
const provider = value.slice(0, idx)
|
|
73
82
|
const model = value.slice(idx + 1)
|
|
74
83
|
const efforts = modelsOf(provider).find((m) => m.model === model)?.efforts ?? []
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
save(keep ? { provider, model } : { provider, model, effort: '' })
|
|
84
|
+
const keep = efforts.some((e) => e.id === cfg[keys.effort])
|
|
85
|
+
return keep ? { [keys.provider]: provider, [keys.model]: model } : { [keys.provider]: provider, [keys.model]: model, [keys.effort]: '' }
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
const pickModel = (value) => save(patchOfModel(value, { provider: 'provider', model: 'model', effort: 'effort' }))
|
|
89
|
+
const pickTaskModel = (value) => save(patchOfModel(value, { provider: 'taskProvider', model: 'taskModel', effort: 'taskEffort' }))
|
|
90
|
+
|
|
80
91
|
if (!cfg) return React.createElement('div', { className: 'vsm-mcp-empty' }, error || '正在读取 AI 补全配置…')
|
|
81
92
|
|
|
82
93
|
const modelValue = cfg.provider && cfg.model ? cfg.provider + '/' + cfg.model : ''
|
|
94
|
+
const taskModelValue = cfg.taskProvider && cfg.taskModel ? cfg.taskProvider + '/' + cfg.taskModel : ''
|
|
95
|
+
const efforts = effortsOf(cfg.provider, cfg.model)
|
|
96
|
+
const taskEfforts = effortsOf(cfg.taskProvider, cfg.taskModel)
|
|
83
97
|
// G7:配置指向已从官方默认列表下架的模型(DSH 0.1.6-alpha.2 移除 V4 Flash 系列)。
|
|
84
98
|
// 仅提示,不擅自改写用户已保存的配置(避免覆盖其意图);清空即回到自动路由。
|
|
85
99
|
const delisted = isDelistedModel(cfg.model)
|
|
100
|
+
const taskDelisted = isDelistedModel(cfg.taskModel)
|
|
86
101
|
return React.createElement('section', { className: 'vsm-lsp-card' },
|
|
87
102
|
React.createElement('h3', null, 'AI 自动补全(实验)'),
|
|
88
103
|
React.createElement('p', { className: 'vsm-lsp-note' }, '编辑停顿后由模型生成内联建议(ghost text),Tab 接受,Alt+\\ 手动触发。每次补全是一次模型调用;建议选择非推理模型,且思考强度选「跟随默认」或最低档以获得更快响应。'),
|
|
@@ -106,6 +121,24 @@ export function AiSettings() {
|
|
|
106
121
|
efforts.map((e) => React.createElement('option', { key: e.id, value: e.id }, e.name))),
|
|
107
122
|
) : null,
|
|
108
123
|
),
|
|
124
|
+
React.createElement('h3', null, 'AI 任务模型(AI 智能整理等非补全调用)'),
|
|
125
|
+
React.createElement('p', { className: 'vsm-lsp-note' }, '独立于补全模型:整理分析可选推理模型 + 较高思考强度,补全仍可选快模型。留空 = 跟随补全模型配置。'),
|
|
126
|
+
(taskDelisted
|
|
127
|
+
? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' },
|
|
128
|
+
'任务模型「' + cfg.taskModel + '」已从 DSH 默认模型列表移除(0.1.6-alpha.2 起)。该模型可能不再可用;将「模型」改回「自动」即可跟随补全配置(现有配置不会被自动改动)。')
|
|
129
|
+
: null),
|
|
130
|
+
React.createElement('div', { className: 'vsm-lsp-row' },
|
|
131
|
+
React.createElement('label', null, '模型',
|
|
132
|
+
React.createElement('select', { value: taskModelValue, disabled: busy || !dir?.providers?.length, onChange: (e) => pickTaskModel(e.target.value) },
|
|
133
|
+
React.createElement('option', { value: '' }, '自动(跟随补全配置)'),
|
|
134
|
+
(dir?.providers ?? []).flatMap((p) => p.models.map((m) => React.createElement('option', { key: p.id + '/' + m.model, value: p.id + '/' + m.model }, p.name + ' · ' + m.name)))),
|
|
135
|
+
),
|
|
136
|
+
taskEfforts.length ? React.createElement('label', null, '思考强度',
|
|
137
|
+
React.createElement('select', { value: cfg.taskEffort, disabled: busy, onChange: (e) => save({ taskEffort: e.target.value }) },
|
|
138
|
+
React.createElement('option', { value: '' }, '跟随默认'),
|
|
139
|
+
taskEfforts.map((e) => React.createElement('option', { key: e.id, value: e.id }, e.name))),
|
|
140
|
+
) : null,
|
|
141
|
+
),
|
|
109
142
|
note && React.createElement('div', { className: 'vsm-compat-item ok' }, React.createElement('span', { className: 'vsm-compat-name' }, note)),
|
|
110
143
|
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
111
144
|
)
|
package/src/client/ui/DiffBox.ts
CHANGED
|
@@ -84,7 +84,7 @@ export function DiffBox(props) {
|
|
|
84
84
|
if (otherFiles.length) {
|
|
85
85
|
othersEl = React.createElement('div', { className: 'edrv-diffbox-others' },
|
|
86
86
|
React.createElement('span', { className: 'edrv-diffrow-tag' }, '其他差异文件 (' + otherFiles.length + '):'),
|
|
87
|
-
otherFiles.map((f) => React.createElement('button', { key: f.path, className: 'edrv-diffrow-file', title: f.path, onClick: () => onOpenOther(f.path) },
|
|
87
|
+
otherFiles.map((f) => React.createElement('button', { key: f.path, className: 'edrv-diffrow-file', title: f.label ?? f.path, onClick: () => onOpenOther(f.path) },
|
|
88
88
|
String(f.path).split(/[\\/]/).pop() + ' (' + f.pending + ')')))
|
|
89
89
|
}
|
|
90
90
|
bodyEl = React.createElement('div', { className: 'edrv-diffbar-body' }, ...rows, ...stale, othersEl)
|
|
@@ -49,8 +49,8 @@ export function DiffLauncher(props) {
|
|
|
49
49
|
if (tab === 'pending') {
|
|
50
50
|
body = (sum.pendingFiles.length === 0
|
|
51
51
|
? React.createElement('div', { className: 'edrv-search-empty' }, '暂无待处理差异(已全部处理)')
|
|
52
|
-
: sum.pendingFiles.map((f) => React.createElement('div', { key: f.path, className: 'edrv-launch-row', title: '打开并跳转到差异 ' + f.path, onClick: () => onOpenFile(f.path) },
|
|
53
|
-
React.createElement('span', { className: 'edrv-launch-path' }, f.path),
|
|
52
|
+
: sum.pendingFiles.map((f) => React.createElement('div', { key: f.path, className: 'edrv-launch-row', title: '打开并跳转到差异 ' + (f.label ?? f.path), onClick: () => onOpenFile(f.path) },
|
|
53
|
+
React.createElement('span', { className: 'edrv-launch-path' }, f.label ?? f.path),
|
|
54
54
|
React.createElement('span', { className: 'edrv-launch-cnt' }, '待处理 ' + f.pending + ' 处'),
|
|
55
55
|
React.createElement('button', { className: 'edrv-btn-mini edrv-btn-keep', onClick: (e) => { e.stopPropagation(); onOpenFile(f.path) } }, '打开'))))
|
|
56
56
|
} else {
|
|
@@ -78,7 +78,9 @@ export function DiffLauncher(props) {
|
|
|
78
78
|
const sm = rec.summary || { accepted: 0, rejected: 0, pending: 0, superseded: false }
|
|
79
79
|
return React.createElement('div', { key: rec.callId, style: { padding: '5px 8px', borderTop: '1px solid var(--dsw-alias-border-l1,#333)' } },
|
|
80
80
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', fontSize: 11, marginBottom: 2 } },
|
|
81
|
-
React.createElement('span', { style: { color: 'var(--dsw-alias-label-primary,#ddd)', fontWeight: 600 } },
|
|
81
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-primary,#ddd)', fontWeight: 600 } },
|
|
82
|
+
rec.toolName === 'remote_ssh_write' ? (rec.create ? '远端新建' : '远端写入')
|
|
83
|
+
: rec.toolName === 'write' ? (rec.create ? '新建' : '写入') : '编辑'),
|
|
82
84
|
React.createElement('span', { style: { color: 'var(--dsw-alias-state-success-primary,#2e9e44)' } }, '采纳 ' + sm.accepted),
|
|
83
85
|
React.createElement('span', { style: { color: 'var(--dsw-alias-state-error-primary,#d9534f)' } }, '拒绝 ' + sm.rejected),
|
|
84
86
|
(rec.superseded ? React.createElement('span', { style: { color: 'var(--dsw-alias-state-warn-primary,#b7791f)' } }, '被覆盖') : null),
|
|
@@ -90,7 +92,7 @@ export function DiffLauncher(props) {
|
|
|
90
92
|
}
|
|
91
93
|
return React.createElement('div', { key: e.at + e.path, style: { border: '1px solid var(--dsw-alias-border-l1,#333)', borderRadius: 8, overflow: 'hidden' } },
|
|
92
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(e.path); loadArchiveDetail(e.path) } } },
|
|
93
|
-
React.createElement('span', { style: { flex: 1, fontSize: 12, color: 'var(--dsw-alias-label-primary,#ddd)', wordBreak: 'break-all' } }, e.path),
|
|
95
|
+
React.createElement('span', { style: { flex: 1, fontSize: 12, color: 'var(--dsw-alias-label-primary,#ddd)', wordBreak: 'break-all' } }, e.displayPath ?? e.path),
|
|
94
96
|
(e.batch != null ? React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#888)' } }, '批次 ' + e.batch) : null),
|
|
95
97
|
(e.reason ? React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-state-warn-primary,#b7791f)' } }, e.reason) : null),
|
|
96
98
|
React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#888)' } }, new Date(e.at).toLocaleString()),
|
|
@@ -16,6 +16,7 @@ import { isMarkdownPath } from '../markdownPreview.js'
|
|
|
16
16
|
import { MarkdownPanel } from '../md/mdPanel.js'
|
|
17
17
|
import { base64ToBytes, bytesToBase64, isPdfPath } from '../pdfPreview.js'
|
|
18
18
|
import { inNativePath, tryNativeOpen } from '../nativeOpenStore.js'
|
|
19
|
+
import { clearPendingNav, putPendingNav, readPendingNav } from '../pendingNav.js'
|
|
19
20
|
import { readBinaryPreview } from '../../shared/rpc.js'
|
|
20
21
|
import {
|
|
21
22
|
clearBaseline,
|
|
@@ -2257,6 +2258,12 @@ export function EditorView(props) {
|
|
|
2257
2258
|
// 「已就绪」会提前定位;随后真实内容到达触发 model 重设/内容写入,Monaco 会重置光标,
|
|
2258
2259
|
// 落点丢失且 pendingFocus 已消费,最终停在 {1,1}。实测(v0.4.6,Ctrl+点击枚举成员):
|
|
2259
2260
|
// opener 正确派发 line=349,落地后编辑器实际停在 1:1 且无高亮;同文件跳转不换内容故不复现。
|
|
2261
|
+
//
|
|
2262
|
+
// ⚠️ 第三次事故(2026-09-23 实测取证):并行改码触发插件 client HMR 热重载
|
|
2263
|
+
// (保留 console 装配日志重复 6-7 轮为证)→ 本组件被重挂载 → 挂载级 pendingFocusRef
|
|
2264
|
+
// 随旧实例销毁 → 「首开只打开文件不落行、热重载平息后第二次才正常」。
|
|
2265
|
+
// 修复:openFileAt 同步写 window.__edrvPendingNav__ 槽(跨热重载存活),本 effect 取
|
|
2266
|
+
// ref || 槽、成功落点后一并清;并加模型身份守卫(模型不是目标文件时不清、等重跑)。
|
|
2260
2267
|
React.useEffect(() => {
|
|
2261
2268
|
const ed = editorRef.current
|
|
2262
2269
|
if (!ed || !contentReady) return
|
|
@@ -2274,11 +2281,24 @@ export function EditorView(props) {
|
|
|
2274
2281
|
ed.focus()
|
|
2275
2282
|
return
|
|
2276
2283
|
}
|
|
2277
|
-
|
|
2284
|
+
// 快路径 = 挂载级 ref;交接路径 = window 槽(热重载重挂载后 ref 已丢、槽仍在)
|
|
2285
|
+
const refPf = pendingFocusRef.current
|
|
2286
|
+
const slotNav = refPf ? null : readPendingNav()
|
|
2287
|
+
const pf = refPf || (slotNav
|
|
2288
|
+
? { path: slotNav.path, region: null, line: slotNav.line ?? null, column: slotNav.column ?? 1, endLine: slotNav.endLine ?? null, endColumn: slotNav.endColumn ?? null }
|
|
2289
|
+
: null)
|
|
2278
2290
|
if (!pf || !sameFile(pf.path, active)) return
|
|
2291
|
+
// 单一清除点:ref 与 window 槽一并处理,防残留二次跳
|
|
2292
|
+
const dropPending = () => {
|
|
2293
|
+
pendingFocusRef.current = null
|
|
2294
|
+
if (pf.line != null) clearPendingNav()
|
|
2295
|
+
}
|
|
2296
|
+
// 模型身份守卫:编辑器模型必须已是目标文件才落点;不匹配则保留 pending(不清),
|
|
2297
|
+
// 依赖变化后的下一次运行再落(model 同步 effect 声明在本 effect 之前、同帧先执行)。
|
|
2279
2298
|
if (pf.line != null) {
|
|
2299
|
+
if (!sameFile(modelPathOf(ed), pf.path)) return
|
|
2280
2300
|
// 搜索命中 / LSP 跳转:直接定位到行/列(不依赖差异区域)并高亮目标区域
|
|
2281
|
-
|
|
2301
|
+
dropPending()
|
|
2282
2302
|
const line = Math.max(1, pf.line)
|
|
2283
2303
|
const column = Math.max(1, pf.column ?? 1)
|
|
2284
2304
|
ed.revealLineInCenter(line)
|
|
@@ -2289,7 +2309,8 @@ export function EditorView(props) {
|
|
|
2289
2309
|
}
|
|
2290
2310
|
const target = pf.region || pendingRegions[0]
|
|
2291
2311
|
if (!target) return
|
|
2292
|
-
|
|
2312
|
+
if (!sameFile(modelPathOf(ed), pf.path)) return
|
|
2313
|
+
dropPending()
|
|
2293
2314
|
ed.revealLineInCenter(Math.max(1, target.start ?? 1))
|
|
2294
2315
|
ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
|
|
2295
2316
|
ed.focus()
|
|
@@ -3122,6 +3143,9 @@ export function EditorView(props) {
|
|
|
3122
3143
|
const normalized = addTabNorm(path, true)
|
|
3123
3144
|
if (!normalized) return
|
|
3124
3145
|
pendingFocusRef.current = { path: normalized, region: null, line: line ?? null, column: column ?? 1, endLine: endLine ?? null, endColumn: endColumn ?? null }
|
|
3146
|
+
// 第三次事故(2026-09-23):插件 HMR 热重载会重挂载本组件、挂载级 ref 随旧实例销毁 →
|
|
3147
|
+
// 落点意图丢失。同步落 window 槽(跨热重载存活)做交接,落点成功后与 ref 一并清。
|
|
3148
|
+
putPendingNav({ path: normalized, line: line ?? null, column: column ?? 1, endLine: endLine ?? null, endColumn: endColumn ?? null })
|
|
3125
3149
|
setFocusRequest((value) => value + 1)
|
|
3126
3150
|
}
|
|
3127
3151
|
|
|
@@ -3525,6 +3549,10 @@ export function EditorView(props) {
|
|
|
3525
3549
|
: null
|
|
3526
3550
|
|
|
3527
3551
|
const sidebarPanels = props.sidebarPanels
|
|
3552
|
+
// SVN 变更查表(文件树徽标 O(1) 查表用):O(n) 建表只随数据/作用域变化重算,
|
|
3553
|
+
// 不再随编辑区每轮渲染重建(条目多时每帧新建几千键的 Record 徒增 GC 压力)。
|
|
3554
|
+
// @author ddj 2026年09月23号
|
|
3555
|
+
const svnChangeMapMemo = React.useMemo(() => svnChangeMapOf(scope), [scope, svnChanges])
|
|
3528
3556
|
const sidebarCtx = {
|
|
3529
3557
|
sessionId,
|
|
3530
3558
|
// 当前会话工作区(反应式 cwd;规则面板项目 Tab 自动匹配)
|
|
@@ -3544,7 +3572,7 @@ export function EditorView(props) {
|
|
|
3544
3572
|
svn: svnStatus,
|
|
3545
3573
|
// SVN 变更:面板列表 + 文件树徽标查表(客户端合成,不改 listDir 契约)
|
|
3546
3574
|
svnChanges,
|
|
3547
|
-
svnChangeMap:
|
|
3575
|
+
svnChangeMap: svnChangeMapMemo,
|
|
3548
3576
|
openSvnDiff: (p) => runSvnDiffBase(p),
|
|
3549
3577
|
openSvnLocalPair: runSvnDiffLocalPair,
|
|
3550
3578
|
refreshSvnChanges: () => refreshSvnChanges(sessionId, scope),
|