dsh-vscode-mode 0.16.0 → 0.18.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 (45) hide show
  1. package/README.md +15 -9
  2. package/lib/client.js +954 -133
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +5026 -4387
  5. package/lib/index.js.map +1 -1
  6. package/locale/en.json +4 -2
  7. package/locale/zh.json +4 -2
  8. package/package.json +4 -2
  9. package/skills/dsh-vscodemode-mcp/SKILL.md +24 -7
  10. package/src/capture.ts +2 -2
  11. package/src/client/compat.ts +2 -1
  12. package/src/client/index.ts +37 -7
  13. package/src/client/localeClient.ts +97 -0
  14. package/src/client/mcpSessionState.ts +108 -0
  15. package/src/client/monaco/lsp/providers.ts +32 -3
  16. package/src/client/officialSidebar.ts +4 -1
  17. package/src/client/state/modelWarmer.ts +198 -0
  18. package/src/client/state/recordsFeed.ts +241 -0
  19. package/src/client/styles/editor.css +5 -1
  20. package/src/client/ui/ConversationDiffDock.ts +10 -19
  21. package/src/client/ui/DiffBadge.ts +12 -19
  22. package/src/client/ui/EditorPane.ts +16 -4
  23. package/src/client/ui/EditorView.ts +114 -51
  24. package/src/client/ui/McpSessionStatus.ts +75 -0
  25. package/src/client/ui/McpSettings.ts +47 -13
  26. package/src/client/ui/PerfSizeBadge.ts +7 -4
  27. package/src/client/ui/PluginDetailSection.ts +82 -0
  28. package/src/dshVersion.ts +4 -2
  29. package/src/fileVersions.ts +2 -2
  30. package/src/lsp/rpc.ts +2 -2
  31. package/src/mcp.ts +1 -0
  32. package/src/mcpBridge.ts +163 -0
  33. package/src/mcpDiscover.ts +38 -0
  34. package/src/mcpPool.ts +245 -0
  35. package/src/mcpProject.ts +15 -12
  36. package/src/mcpRuntime.ts +301 -255
  37. package/src/registry.ts +15 -2
  38. package/src/rpc.ts +15 -3
  39. package/src/rules.ts +9 -4
  40. package/src/search/fallback.ts +2 -2
  41. package/src/search/ripgrep.ts +2 -2
  42. package/src/shared/mcp.ts +47 -0
  43. package/src/shared/rpc.ts +5 -1
  44. package/src/store.ts +26 -1
  45. package/src/svn.ts +2 -2
package/src/registry.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import type { DiffRecord } from './shared/types.js'
7
7
  import type { Ctx, Session } from './store.js'
8
- import { loadBucket } from './store.js'
8
+ import { cwdOfSession, loadBucket } from './store.js'
9
9
 
10
10
  /** cwd → 记录桶(内存缓存;随会话销毁清理)。 */
11
11
  export type Registry = Map<string, Map<string, DiffRecord>>
@@ -19,7 +19,20 @@ export function sessionOf(ctx: Ctx, sessionId?: string): Session | undefined {
19
19
  return live.length === 1 ? live[0] : undefined
20
20
  }
21
21
 
22
- /** 会话工作区 cwd。 */
22
+ /**
23
+ * 会话工作区 cwd(跟随会话内 cd;服务缺失时回落 header.cwd)。
24
+ * 定义在 store.ts 以避免 registry ↔ store 循环依赖,此处仅转出为会话助手入口。
25
+ * @author ddj 2026年10月19号
26
+ */
27
+ export { cwdOfSession }
28
+
29
+ /**
30
+ * 会话头部的原始 cwd(不跟随 cd)。仅用于会话销毁等无上下文场景:
31
+ * 此时投影可能已随会话卸载,读 header 才能保证拿到建会话时的目录。
32
+ * @author ddj 2026年10月19号
33
+ * @param session 目标会话
34
+ * @returns 绝对路径或 null
35
+ */
23
36
  export function cwdOf(session: Session | undefined): string | null {
24
37
  return session?.header?.cwd ?? null
25
38
  }
package/src/rpc.ts CHANGED
@@ -25,7 +25,7 @@ import { archiveEntryFor, markDecision, recordResolved, reconstructOriginal } fr
25
25
  import { clearRevertState, listArchives, pageArchives, readArchives, revertState, setRevertState, startReverts } from './archiveDb.js'
26
26
  import { log } from './log.js'
27
27
  import type { Registry } from './registry.js'
28
- import { bucketOf, cwdOf, sessionOf } from './registry.js'
28
+ import { bucketOf, cwdOfSession, sessionOf } from './registry.js'
29
29
  import type { SearchOrchestrator } from './search/orchestrator.js'
30
30
  import { newSearcher } from './search/orchestrator.js'
31
31
  import type { ContentSearcher } from './search/content.js'
@@ -33,6 +33,7 @@ import { newContentSearcher } from './search/content.js'
33
33
  import { restoreFile, revertCall, revertHunk, revertHunks, type Result } from './revert.js'
34
34
  import { rulesList, rulesRead, rulesRemove, rulesSave, rulesToggle } from './rules.js'
35
35
  import { isSnippetFilePath, snippetsEntries, snippetsList, snippetsRead, snippetsRemove, snippetsSave } from './snippets.js'
36
+ import { runtimeOf } from './mcpRuntime.js'
36
37
  import { listMcp, refreshMcp, removeMcp, saveMcp, toggleMcp } from './mcp.js'
37
38
  import { listProjects, projectRefresh, projectRemove, projectSave, projectToggle, mcpSnapshot } from './mcpProject.js'
38
39
  import { normalizeFileOpenTool, FILE_OPEN_DEFAULT, FILE_OPEN_SETTINGS_NS, sectionOf, updateSection } from './fileOpenSettings.js'
@@ -100,7 +101,7 @@ async function requireSession(ctx: Ctx, sessionId: string | undefined): Promise<
100
101
  > {
101
102
  const session = sessionOf(ctx, sessionId)
102
103
  if (!session) return { err: SESSION_MISSING_ERROR }
103
- const cwd = cwdOf(session)
104
+ const cwd = cwdOfSession(ctx, session)
104
105
  if (!cwd) return { err: SESSION_NO_WORKSPACE_ERROR }
105
106
  return { session, cwd }
106
107
  }
@@ -1170,6 +1171,7 @@ export function buildHandlers(
1170
1171
  },
1171
1172
  'mcp.list': async () => ({ ok: true, ...listMcp(ctx) }),
1172
1173
  'mcp.snapshot': async () => ({ ok: true, ...mcpSnapshot(ctx) }),
1174
+ 'mcp.sessionSnapshot': async () => ({ ok: true, ...runtimeOf(ctx).sessionSnapshot() }),
1173
1175
  'mcp.save': async (args) => {
1174
1176
  try { return { ok: true, server: await saveMcp(ctx, args.config) } }
1175
1177
  catch (error) { return { ok: false, error: String(error) } }
@@ -1190,8 +1192,18 @@ export function buildHandlers(
1190
1192
  try { return { ok: true, ...await listProjects(ctx) } }
1191
1193
  catch (error) { return { ok: false, error: String(error) } }
1192
1194
  },
1195
+ 'mcp.projectConnect': async (args) => {
1196
+ const runtime = runtimeOf(ctx)
1197
+ const agent = [...runtime.agents.keys()].find((owner) => owner.session?.id === args.sessionId)
1198
+ if (!agent || runtime.workspace(agent) !== args.workspacePath) return { ok: false, error: '请先打开此项目的活跃会话;不会激活历史会话' }
1199
+ try {
1200
+ await runtime.connect(agent, args.serverName)
1201
+ const project = mcpSnapshot(ctx).projects.find((item) => item.workspacePath === args.workspacePath)!
1202
+ return { ok: true, project }
1203
+ } catch { return { ok: false, error: '项目 MCP 连接失败,请查看会话状态和 Host 日志' } }
1204
+ },
1193
1205
  'mcp.projectSave': async (args) => {
1194
- try { return { ok: true, project: await projectSave(ctx, args.workspacePath, args.serverName, args.config) } }
1206
+ try { return { ok: true, project: await projectSave(ctx, args.workspacePath, args.serverName, args.config, args.policy) } }
1195
1207
  catch (error) { return { ok: false, error: String(error) } }
1196
1208
  },
1197
1209
  'mcp.projectRemove': async (args) => {
package/src/rules.ts CHANGED
@@ -16,6 +16,7 @@ import { join, resolve } from 'node:path'
16
16
  import { dshHome } from './paths.js'
17
17
  import type { RuleInfo, RuleProject, RuleRefInput, RuleScope, RuleSaveInput, UserRuleOrigin } from './shared/rules.js'
18
18
  import type { Ctx } from './store.js'
19
+ import { cwdOfSession } from './store.js'
19
20
 
20
21
  // --region 常量与目录定位
21
22
  /** 规则文件名白名单:字母数字开头,仅字母数字点横下划线,.mdc 后缀(天然拒绝路径分隔符)。 */
@@ -550,13 +551,17 @@ export function readUserRulesSync(dirs = userRuleDirs()): LoadedRule[] {
550
551
  return mergeUserRules(current, legacy, (rule) => rule.info.file)
551
552
  }
552
553
 
553
- /** 从装配上下文解析会话工作区 cwd(agent.id → sessions → header.cwd;缺链返回 null)。 */
554
+ /**
555
+ * 从装配上下文解析会话工作区 cwd(agent.id → sessions → 当前目录;缺链返回 null)。
556
+ * @author ddj 2026年10月19号
557
+ * @param ctx DSH 上下文
558
+ * @param asm 装配上下文(含 agent.id)
559
+ * @returns 项目规则根的绝对路径或 null
560
+ */
554
561
  function cwdFromAssemble(ctx: Ctx, asm: { agent?: { id?: unknown } } | undefined): string | null {
555
562
  const id = asm?.agent?.id
556
563
  if (typeof id !== 'string' || !id) return null
557
- const session = ctx.get('sessions')?.get?.(id)
558
- const cwd = session?.header?.cwd
559
- return typeof cwd === 'string' && cwd ? cwd : null
564
+ return cwdOfSession(ctx, ctx.get('sessions')?.get?.(id))
560
565
  }
561
566
 
562
567
  /**
@@ -4,7 +4,7 @@
4
4
  * 作者 ddj 2026年08月24号
5
5
  */
6
6
  import type { Ctx, Session } from '../store.js'
7
- import { policyOf } from '../store.js'
7
+ import { cwdOfSession, policyOf } from '../store.js'
8
8
  import { pathText, pathMatch, prepareQuery } from './query.js'
9
9
  import type { PreparedQuery, WorkspaceSearchInput, WorkspaceSearchProvider, WorkspaceSearchResult } from './types.js'
10
10
 
@@ -61,7 +61,7 @@ export async function searchFallback(input: WorkspaceSearchInput): Promise<Works
61
61
  const fs = input.ctx.get('fs') as SearchFs | undefined
62
62
  if (!fs) throw new Error('缺少 fs')
63
63
  const policy = policyOf(input.ctx, input.session)
64
- const cwd = input.session?.header?.cwd
64
+ const cwd = cwdOfSession(input.ctx, input.session)
65
65
  const rootTarget = await fs.resolve(policy?.workspaceRoot ?? '.', cwd ? { cwd, signal: input.signal } : { signal: input.signal })
66
66
  const signal = input.signal ?? new AbortController().signal
67
67
  const files: string[] = []
@@ -7,7 +7,7 @@ import { existsSync } from 'node:fs'
7
7
  import { join } from 'node:path'
8
8
  import { createRequire } from 'node:module'
9
9
  import type { Ctx, Session } from '../store.js'
10
- import { policyOf } from '../store.js'
10
+ import { cwdOfSession, policyOf } from '../store.js'
11
11
  import { prepareQuery, pathText } from './query.js'
12
12
  import type { WorkspaceSearchInput, WorkspaceSearchProvider, WorkspaceSearchResult } from './types.js'
13
13
 
@@ -127,7 +127,7 @@ export async function searchRoot(ctx: Ctx, session: Session): Promise<string> {
127
127
  const fs = ctx.get('fs')
128
128
  if (!fs) throw new Error('缺少 fs')
129
129
  const policy = policyOf(ctx, session)
130
- const cwd = session?.header?.cwd
130
+ const cwd = cwdOfSession(ctx, session)
131
131
  const rootTarget = await fs.resolve(policy?.workspaceRoot ?? '.', cwd ? { cwd } : {})
132
132
  return fs.processPath(rootTarget)
133
133
  }
package/src/shared/mcp.ts CHANGED
@@ -1,6 +1,50 @@
1
1
  /** dsh-vscode-mode MCP 管理共享数据契约。作者 ddj 2026年08月22号 */
2
2
 
3
3
  export type MpcTransport = 'stdio' | 'streamable-http'
4
+ /** Project initialization only; ready does not establish transport health. */
5
+ export type McpPhase = 'preparing' | 'idle' | 'queued' | 'loading' | 'starting' | 'stopping' | 'ready' | 'error' | 'none'
6
+
7
+ export interface McpPolicy {
8
+ startup: 'on-demand' | 'background'
9
+ sharing: 'agent' | 'workspace'
10
+ startupTimeoutMs: number
11
+ }
12
+
13
+ /** @public @author ddj 2026年10月20号 @param input Project-only policy. @returns Validated defaults. @throws Invalid explicit policy. */
14
+ export function parseMcpPolicy(input?: unknown): McpPolicy {
15
+ if (input !== undefined && (!input || typeof input !== 'object' || Array.isArray(input))) throw new Error('vscodeMode 必须是对象')
16
+ const policy = (input ?? {}) as Partial<McpPolicy>
17
+ const startup = policy.startup ?? 'on-demand'
18
+ const sharing = policy.sharing ?? 'agent'
19
+ const startupTimeoutMs = policy.startupTimeoutMs ?? 60000
20
+ if (!['on-demand', 'background'].includes(startup)) throw new Error('不支持的 MCP startup')
21
+ if (!['agent', 'workspace'].includes(sharing)) throw new Error('不支持的 MCP sharing')
22
+ if (!Number.isInteger(startupTimeoutMs) || startupTimeoutMs < 1000 || startupTimeoutMs > 300000) throw new Error('MCP startupTimeoutMs 必须为 1000–300000 的整数')
23
+ return { startup, sharing, startupTimeoutMs }
24
+ }
25
+ export interface McpSessionServer {
26
+ queuedMs?: number
27
+ connectMs?: number
28
+ firstCallMs?: number
29
+ reused?: boolean
30
+ serverName: string
31
+ phase: McpPhase
32
+ startedAt: number
33
+ phaseAt: number
34
+ finishedAt?: number
35
+ error?: string
36
+ }
37
+ export interface McpSessionStatus {
38
+ configMs?: number
39
+ sessionId: string
40
+ phase: McpPhase
41
+ startedAt: number
42
+ phaseAt: number
43
+ finishedAt?: number
44
+ error?: string
45
+ servers: McpSessionServer[]
46
+ }
47
+
4
48
  export type MpcStatus = 'connected' | 'connecting' | 'error' | 'disabled' | 'configured' | 'unverified'
5
49
 
6
50
  export interface MpcTool {
@@ -32,6 +76,8 @@ export interface MpcServer {
32
76
  status: MpcStatus
33
77
  /** Mounted official plugin fibers, not verified live connections. */
34
78
  instanceCount?: number
79
+ sessionCount?: number
80
+ policy?: McpPolicy
35
81
  toolCount: number
36
82
  tools: MpcTool[]
37
83
  error?: string
@@ -50,6 +96,7 @@ export interface MpcProject {
50
96
 
51
97
  /** 项目级 MCP 保存入参:config 为该 serverName 的传输配置(不含归属信息)。 */
52
98
  export interface MpcProjectSaveInput {
99
+ policy?: McpPolicy
53
100
  workspacePath: string
54
101
  serverName: string
55
102
  config: MpcConfig
package/src/shared/rpc.ts CHANGED
@@ -12,7 +12,7 @@ import type {
12
12
  RecordView,
13
13
  StaleHunk,
14
14
  } from './types.js'
15
- import type { MpcConfig, MpcProject, MpcProjectSaveInput, MpcServer } from './mcp.js'
15
+ import type { McpSessionStatus, MpcConfig, MpcProject, MpcProjectSaveInput, MpcServer } from './mcp.js'
16
16
  import type { CompatReport, DevFormInfo } from './compat.js'
17
17
  import type { ShellIntegrationStatus, UnityListPayload, UnityProjectEntry } from './integration.js'
18
18
  import type { RuleInfo, RuleProject, RuleRefInput, RuleSaveInput } from './rules.js'
@@ -399,11 +399,13 @@ export interface RpcRequestMap {
399
399
  'edrv.fsMove': { sessionId?: string; from: string; toDir: string }
400
400
  'mcp.list': {}
401
401
  'mcp.snapshot': {}
402
+ 'mcp.sessionSnapshot': {}
402
403
  'mcp.save': { config: MpcConfig }
403
404
  'mcp.remove': { id: string }
404
405
  'mcp.toggle': { id: string; enabled: boolean }
405
406
  'mcp.refresh': { id: string }
406
407
  'mcp.projects': {}
408
+ 'mcp.projectConnect': { sessionId: string; serverName: string; workspacePath: string }
407
409
  'mcp.projectSave': MpcProjectSaveInput
408
410
  'mcp.projectRemove': { workspacePath: string; serverName: string }
409
411
  'mcp.projectToggle': { workspacePath: string; serverName: string; enabled: boolean }
@@ -565,11 +567,13 @@ export interface RpcOkMap {
565
567
  'edrv.fsMove': { from: string; to: string }
566
568
  'mcp.list': { servers: MpcServer[] }
567
569
  'mcp.snapshot': { servers: MpcServer[]; projects: MpcProject[] }
570
+ 'mcp.sessionSnapshot': { sessions: McpSessionStatus[]; sampledAt: number }
568
571
  'mcp.save': { server: MpcServer }
569
572
  'mcp.remove': object
570
573
  'mcp.toggle': { server: MpcServer }
571
574
  'mcp.refresh': { server: MpcServer }
572
575
  'mcp.projects': { projects: MpcProject[] }
576
+ 'mcp.projectConnect': { project: MpcProject }
573
577
  'mcp.projectSave': { project: MpcProject }
574
578
  'mcp.projectRemove': { project: MpcProject }
575
579
  'mcp.projectToggle': { project: MpcProject }
package/src/store.ts CHANGED
@@ -33,10 +33,35 @@ export function policyOf(ctx: Ctx, session: Session | undefined): any {
33
33
  return session ? svc.resolve({ session }) : svc.resolve()
34
34
  }
35
35
 
36
+ /**
37
+ * 读取会话当前工作目录:优先 workingDirectory 投影(DSH 0.2.1 起支持会话内 cd),
38
+ * 服务缺失或投影未就绪时回落到 header.cwd(会话创建时的原始目录)。
39
+ * 服务一律可选探测:不得写入模块 inject,否则旧版 DSH 会停等一个不存在的服务。
40
+ * @author ddj 2026年10月19号
41
+ * @param ctx DSH 上下文(用于可选探测 workingDirectory 服务)
42
+ * @param session 目标会话
43
+ * @returns 绝对路径;无法确定时 null
44
+ */
45
+ export function cwdOfSession(ctx: Ctx, session: Session | undefined): string | null {
46
+ const headerCwd = session?.header?.cwd
47
+ const fallback = typeof headerCwd === 'string' && headerCwd ? headerCwd : null
48
+ if (!session) return null
49
+
50
+ const service = ctx?.get?.('workingDirectory')
51
+ if (!service || typeof service.get !== 'function') return fallback
52
+ try {
53
+ const current = service.get(session)
54
+ return typeof current === 'string' && current ? current : fallback
55
+ } catch {
56
+ // 会话已销毁或投影缺失:回落原始目录,不让新能力变成新的故障点
57
+ return fallback
58
+ }
59
+ }
60
+
36
61
  /** 按会话 cwd 解析目标路径。 */
37
62
  export async function resolveTarget(ctx: Ctx, session: Session | undefined, path: string): Promise<string> {
38
63
  const fs = ctx.get('fs')
39
- const cwd = session?.header?.cwd
64
+ const cwd = cwdOfSession(ctx, session)
40
65
  return fs.resolve(path, cwd ? { cwd } : {})
41
66
  }
42
67
 
package/src/svn.ts CHANGED
@@ -10,7 +10,7 @@ import { readdir, stat } from 'node:fs/promises'
10
10
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
11
11
  import { tmpdir } from 'node:os'
12
12
  import type { Ctx, Session } from './store.js'
13
- import { cwdOf, sessionOf } from './registry.js'
13
+ import { cwdOfSession, sessionOf } from './registry.js'
14
14
  import type { SvnAction, SvnAiPlan, SvnChangeEntry, SvnConflictArtifact, SvnFeature, SvnIgnoreItem, SvnItemStatus, SvnLogEntry, SvnRemoteOutdatedEntry, SvnStatusPayload, SvnSumEntry } from './shared/svn.js'
15
15
  import { SESSION_MISSING_ERROR, SESSION_NO_WORKSPACE_ERROR } from './shared/rpc.js'
16
16
  import { AI_PLAN_DIFF_CAP, AI_PLAN_PATHS_CAP, BATCH_PATHS_CAP, FILE_SIZES_CAP, SVN_CLI_DEFAULT, TORTOISE_EXE, contentGuardOf, normalizeAiPlan, svnChangelistNameErrorOf, tortoiseLaunchArgv } from './shared/svn.js'
@@ -463,7 +463,7 @@ async function requireSvnSession(
463
463
  ): Promise<{ cwd: string } | { err: string }> {
464
464
  const session: Session | undefined = sessionOf(ctx, sessionId)
465
465
  if (!session) return { err: SESSION_MISSING_ERROR }
466
- const cwd = cwdOf(session)
466
+ const cwd = cwdOfSession(ctx, session)
467
467
  if (!cwd) return { err: SESSION_NO_WORKSPACE_ERROR }
468
468
  return { cwd }
469
469
  }