dsh-vscode-mode 0.7.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.
Files changed (60) hide show
  1. package/README.md +37 -6
  2. package/assets/icon.svg +7 -0
  3. package/cordis.patch.yml +16 -0
  4. package/lib/client.js +2804 -300
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1975 -191
  7. package/lib/index.js.map +1 -1
  8. package/locale/en.json +4 -0
  9. package/locale/zh.json +4 -0
  10. package/package.json +4 -2
  11. package/src/ai/inline.ts +19 -2
  12. package/src/ai/svnTriage.ts +150 -0
  13. package/src/capture.ts +225 -51
  14. package/src/client/addToConversation.ts +203 -1
  15. package/src/client/compat.ts +134 -4
  16. package/src/client/imagePreview.ts +76 -2
  17. package/src/client/index.ts +65 -36
  18. package/src/client/monaco/lsp/lspClient.ts +46 -0
  19. package/src/client/monaco/lsp/providers.ts +245 -1
  20. package/src/client/nativeOpenStore.ts +91 -0
  21. package/src/client/officialSidebar.ts +11 -45
  22. package/src/client/outline/index.ts +1 -1
  23. package/src/client/pdf/pdfPanel.ts +18 -0
  24. package/src/client/pendingNav.ts +141 -0
  25. package/src/client/settingsContext.ts +6 -4
  26. package/src/client/sidebar/panels/FileExplorer.ts +2 -15
  27. package/src/client/sidebar/panels/SvnPanel.ts +682 -72
  28. package/src/client/sidebar/panels/index.ts +1 -1
  29. package/src/client/sidebar/panels/svnListBudget.ts +58 -0
  30. package/src/client/state/records.ts +7 -1
  31. package/src/client/styles/editor.css +47 -5
  32. package/src/client/svnStatus.ts +206 -1
  33. package/src/client/svnStore.ts +6 -1
  34. package/src/client/ui/AiSettings.ts +47 -14
  35. package/src/client/ui/DiffBox.ts +1 -1
  36. package/src/client/ui/DiffLauncher.ts +6 -4
  37. package/src/client/ui/EditorView.ts +149 -22
  38. package/src/client/ui/McpSettings.ts +30 -0
  39. package/src/client/ui/PerfSettings.ts +4 -1
  40. package/src/client/ui/SvnAiPlanDialog.ts +293 -0
  41. package/src/client/ui/icons.ts +91 -0
  42. package/src/compat.ts +26 -5
  43. package/src/dshVersion.ts +2 -1
  44. package/src/fileOpenSettings.ts +274 -49
  45. package/src/index.ts +18 -1
  46. package/src/lsp/client.ts +24 -0
  47. package/src/lsp/rpc.ts +57 -0
  48. package/src/lsp/server.ts +272 -2
  49. package/src/model.ts +2 -1
  50. package/src/perf.ts +90 -4
  51. package/src/remoteWorkspace.ts +177 -0
  52. package/src/routes.ts +13 -1
  53. package/src/rpc.ts +115 -39
  54. package/src/shared/ai.ts +12 -0
  55. package/src/shared/lsp.ts +79 -0
  56. package/src/shared/nativeOpen.ts +91 -0
  57. package/src/shared/rpc.ts +162 -2
  58. package/src/shared/svn.ts +296 -10
  59. package/src/shared/types.ts +5 -1
  60. package/src/svn.ts +235 -13
@@ -6,7 +6,7 @@
6
6
  * 作者 ddj 2026-08-26 / 2026-09-03 / 2026-09-10 / 2026-09-16
7
7
  */
8
8
  import React from 'react'
9
- import { IconBranchOutline16, IconFolderOpenOutline16, IconListPenOutline16, IconSearchOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
9
+ import { IconBranchOutline16, IconFolderOpenOutline16, IconListPenOutline16, IconSearchOutline16 } from '../../ui/icons.js'
10
10
  import { dapStore } from '../../dap/store.js'
11
11
  import { FileExplorer } from './FileExplorer.js'
12
12
  import { SearchPanel } from './SearchPanel.js'
@@ -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) files.push({ path, recs, pending: pendingCount(recs) })
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 }
@@ -71,9 +71,11 @@
71
71
  [data-edrv-view] .edrv-imgview { flex: 1 1 auto; min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
72
72
  [data-edrv-view] .edrv-imgview-bar { display: flex; align-items: center; gap: 8px; height: 34px; padding: 0 10px; flex-shrink: 0; border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); background: var(--dsw-alias-bg-layer-1, transparent); }
73
73
  [data-edrv-view] .edrv-imgview-meta { font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); white-space: nowrap; }
74
- [data-edrv-view] .edrv-imgview-stage { flex: 1 1 auto; min-height: 0; display: flex; align-items: center; justify-content: center; overflow: auto; padding: 14px; background-color: var(--dsw-alias-bg-layer-1, #f0f4f4); background-image: conic-gradient(rgba(128,128,128,.14) 25%, transparent 0 50%, rgba(128,128,128,.14) 0 75%, transparent 0); background-size: 16px 16px; }
75
- [data-edrv-view] .edrv-imgview-img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 4px; box-shadow: 0 2px 12px rgba(31,41,51,.18); }
76
- [data-edrv-view] .edrv-imgview-broken { display: flex; flex-direction: column; align-items: center; gap: 10px; font-size: 13px; color: var(--dsw-alias-label-tertiary, #777); }
74
+ /* 图片预览:居中交给子项 margin:auto(auto 外边距优先吃正向空闲空间;溢出时空闲为负 → 外边距归 0、
75
+ flex-start 起点对齐 → 放大后四边均可滚动到达,规避 justify-content:center 双侧溢出不可达问题) */
76
+ [data-edrv-view] .edrv-imgview-stage { flex: 1 1 auto; min-height: 0; display: flex; overflow: auto; padding: 14px; background-color: var(--dsw-alias-bg-layer-1, #f0f4f4); background-image: conic-gradient(rgba(128,128,128,.14) 25%, transparent 0 50%, rgba(128,128,128,.14) 0 75%, transparent 0); background-size: 16px 16px; }
77
+ [data-edrv-view] .edrv-imgview-img { max-width: 100%; max-height: 100%; flex-shrink: 0; margin: auto; align-self: flex-start; object-fit: contain; border-radius: 4px; box-shadow: 0 2px 12px rgba(31,41,51,.18); }
78
+ [data-edrv-view] .edrv-imgview-broken { display: flex; flex-direction: column; align-items: center; gap: 10px; font-size: 13px; color: var(--dsw-alias-label-tertiary, #777); margin: auto; }
77
79
  [data-edrv-view] .edrv-pdf-host { flex: 1 1 auto; min-width: 0; min-height: 0; position: relative; overflow: hidden; }
78
80
  [data-edrv-view] .edrv-pdf-host > .edrv-pdf { position: absolute; inset: 0; }
79
81
  [data-edrv-view] .edrv-pdf { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
@@ -299,10 +301,25 @@
299
301
  [data-edrv-view] .edrv-svn-check input { margin: 0; cursor: pointer; }
300
302
  [data-edrv-view] .edrv-svn-bulk { display: flex; align-items: center; gap: 6px; padding: 0 10px 6px 12px; flex-shrink: 0; }
301
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; }
302
317
  [data-edrv-view] .edrv-svn-list { flex: 1; min-height: 0; overflow: auto; padding: 2px 6px 12px; font-size: 12px; }
303
- [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); }
304
321
  [data-edrv-view] .edrv-svn-group-n { margin-left: auto; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-weight: 400; }
305
- [data-edrv-view] .edrv-svn-row { display: flex; align-items: center; gap: 6px; height: 22px; padding: 0 6px; border-radius: 5px; cursor: default; white-space: nowrap; }
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; }
306
323
  [data-edrv-view] .edrv-svn-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
307
324
  [data-edrv-view] .edrv-svn-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-primary, #1f2933); }
308
325
  [data-edrv-view] .edrv-svn-acts { display: none; align-items: center; gap: 4px; flex-shrink: 0; }
@@ -365,6 +382,31 @@
365
382
  [data-edrv-view] .edrv-svn-patch-warn { color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; }
366
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; }
367
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); }
368
410
  [data-edrv-view] .edrv-svn-sum-form { display: flex; align-items: center; gap: 10px; padding-bottom: 8px; }
369
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; }
370
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; }
@@ -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
  /**
@@ -90,7 +90,12 @@ export function createSvnStore<M extends RpcMethod, T>(config: SvnStoreConfig<M,
90
90
 
91
91
  const ensure = (sessionId: string | undefined, key: string | null | undefined, force = false, extra?: Record<string, unknown>): void => {
92
92
  if (!key || inFlight.has(key)) return
93
- if (!force && cache.has(key)) return
93
+ if (!force && cache.has(key)) {
94
+ // 缓存命中也要把跨 scope 镜像切回该 key:否则从非 SVN 工作区切回后,
95
+ // latest()(命令栏 svnCurrentStatus 等只读判定的数据源)仍残留上一个工作区的载荷
96
+ latestValue = cache.get(key) as T
97
+ return
98
+ }
94
99
  const fixed = config.args ? config.args(key) : {}
95
100
  const task = rpc(config.method, { ...fixed, ...extra, sessionId, force: force || undefined } as RpcRequestMap[M])
96
101
  .then((res) => {
@@ -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) setCfg({ enabled: c.enabled, provider: c.provider, model: c.model, effort: c.effort })
29
- else setError(c.error || '配置读取失败')
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
- /** 档位可选项:模型无 reasoning 元数据 → 空数组(选择器隐藏)。 */
44
- const efforts = (() => {
45
- const models = modelsOf(cfg?.provider)
46
- return models.find((m) => m.model === cfg?.model)?.efforts ?? models[0]?.efforts ?? []
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({ enabled: res.enabled, provider: res.provider, model: res.model, effort: res.effort })
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
- const pickModel = (value) => {
69
- // 「自动」:provider/model 清空(host 自动取目录首个);档位保留
70
- if (!value) return save({ provider: '', model: '' })
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
- const keep = efforts.some((e) => e.id === cfg.effort)
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
  )
@@ -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 } }, rec.toolName === 'write' ? (rec.create ? '新建' : '写入') : '编辑'),
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()),