dsh-vscode-mode 0.1.17 → 0.1.20
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 +17 -7
- package/lib/client.js +1261 -280
- package/lib/client.js.map +1 -1
- package/lib/index.js +169 -23
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/addToConversation.ts +211 -0
- package/src/client/diffDock.ts +52 -0
- package/src/client/diffDockStore.ts +87 -0
- package/src/client/editorLayout.ts +19 -0
- package/src/client/events.ts +40 -11
- package/src/client/index.ts +48 -5
- package/src/client/sidebar/SidebarView.ts +73 -0
- package/src/client/sidebar/panels/FileExplorer.ts +161 -0
- package/src/client/sidebar/panels/index.ts +24 -0
- package/src/client/sidebar/registry.ts +52 -0
- package/src/client/sidebar/types.ts +31 -0
- package/src/client/styles/editor.css +127 -5
- package/src/client/ui/ConversationDiffDock.ts +84 -0
- package/src/client/ui/DiffBox.ts +78 -29
- package/src/client/ui/EditorView.ts +375 -68
- package/src/rpc.ts +84 -18
- package/src/shared/rpc.ts +29 -1
- package/src/store.ts +6 -2
- package/src/tree.ts +62 -0
package/package.json
CHANGED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 把文件/选中内容注入会话对话输入框(composer draft)。
|
|
3
|
+
* 复用 DSH 官方输入门面:ctx.conversation.input.for(sessions.scope(sessionId)) → SessionInput。
|
|
4
|
+
* 引用 chip:insertReference(reference source,发送时序列化为 @path 文本);忙态降级为纯文本 setDraft。
|
|
5
|
+
* 纯函数部分 node 可单测。
|
|
6
|
+
* 作者 ddj 2026-08-25
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 文件引用可选的行区间(选中内容用)。 */
|
|
10
|
+
export interface RefRange {
|
|
11
|
+
startLine: number
|
|
12
|
+
endLine: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 引用动作的返回状态。 */
|
|
16
|
+
export type AddOutcome = 'ok' | 'busy' | 'unavailable'
|
|
17
|
+
|
|
18
|
+
/** DSH reference source 名(dsh-client-ui-reference 注册的 @file/@session 统一源)。 */
|
|
19
|
+
const REF_SOURCE = 'reference'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 生成 DSH @file 语法引用串:cwd 相对化、\ → /、含空白时按 @"path" 语法加引号。
|
|
23
|
+
* cwd 外/无法相对化的路径回退原路径。
|
|
24
|
+
* @author ddj 2026年08月25号
|
|
25
|
+
* @param path 打开的文件路径(相对 cwd 或绝对)
|
|
26
|
+
* @param cwd 会话工作区目录(可选)
|
|
27
|
+
* @returns 引用串(如 @src/index.ts 或 @"a b.ts")
|
|
28
|
+
*/
|
|
29
|
+
export function mentionOf(path: string, cwd: string | undefined): string {
|
|
30
|
+
const raw = String(path)
|
|
31
|
+
let rel = raw
|
|
32
|
+
if (cwd) {
|
|
33
|
+
const base = String(cwd).replace(/[\\/]+$/, '')
|
|
34
|
+
const up = raw.replace(/\\/g, '/')
|
|
35
|
+
const baseUp = base.replace(/\\/g, '/')
|
|
36
|
+
if (up.startsWith(baseUp + '/')) rel = up.slice(baseUp.length + 1)
|
|
37
|
+
else if (up === baseUp) rel = ''
|
|
38
|
+
}
|
|
39
|
+
rel = rel.replace(/\\/g, '/').replace(/^\.\//, '')
|
|
40
|
+
if (!rel) rel = raw.replace(/\\/g, '/')
|
|
41
|
+
if (/[\u0000-\u001f\u007f-\u009f"]/u.test(rel)) return '@' + raw
|
|
42
|
+
return /\s/u.test(rel) ? `@"${rel}"` : `@${rel}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 引用标签:文件名 + 可选行区间(chip 内展示,不带 @ 前缀)。
|
|
46
|
+
* @author ddj 2026年08月25号 */
|
|
47
|
+
export function labelOf(path: string, range: RefRange | undefined): string {
|
|
48
|
+
const base = String(path).split(/[\\/]/).pop() || String(path)
|
|
49
|
+
if (!range || range.endLine < range.startLine) return base
|
|
50
|
+
return range.startLine === range.endLine
|
|
51
|
+
? `${base} L${range.startLine}`
|
|
52
|
+
: `${base} L${range.startLine}-${range.endLine}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 引用 ref:引用串 + 可选行区间(序列化直出,agent 据此读取对应片段)。
|
|
56
|
+
* @author ddj 2026年08月25号 */
|
|
57
|
+
export function refOf(mention: string, range: RefRange | undefined): string {
|
|
58
|
+
if (!range || range.endLine < range.startLine) return mention
|
|
59
|
+
return range.startLine === range.endLine
|
|
60
|
+
? `${mention} L${range.startLine}`
|
|
61
|
+
: `${mention} L${range.startLine}-${range.endLine}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 构造 DSH 文件引用插入载荷(source=reference,发送序列化为 mention(+range) 文本)。 */
|
|
65
|
+
export function buildFileRef(
|
|
66
|
+
path: string,
|
|
67
|
+
cwd: string | undefined,
|
|
68
|
+
range?: RefRange,
|
|
69
|
+
): { reference: ReferenceInsertLike; mention: string } {
|
|
70
|
+
const mention = mentionOf(path, cwd)
|
|
71
|
+
return {
|
|
72
|
+
mention,
|
|
73
|
+
reference: {
|
|
74
|
+
source: REF_SOURCE,
|
|
75
|
+
ref: refOf(mention, range),
|
|
76
|
+
label: labelOf(path, range),
|
|
77
|
+
appearance: 'file',
|
|
78
|
+
clipboardText: mention,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 输入门面最小形状(运行时来自 ctx.conversation.input,避免引入新类型依赖)。 */
|
|
84
|
+
export interface InputLike {
|
|
85
|
+
state: { getSnapshot: () => { draft: string; draftRev: number } }
|
|
86
|
+
insertReference: (ref: ReferenceInsertLike, span: TokenSpanLike) => boolean
|
|
87
|
+
setDraft: (text: string) => void
|
|
88
|
+
notify: (level: 'info' | 'error', text: string) => void
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** ReferenceInsert 结构形状(与 dsh-client-ui-input-trigger 类型一致)。 */
|
|
92
|
+
export interface ReferenceInsertLike {
|
|
93
|
+
source: string
|
|
94
|
+
ref: string
|
|
95
|
+
label: string
|
|
96
|
+
appearance?: 'session' | 'file' | 'folder'
|
|
97
|
+
clipboardText: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** TokenSpan 结构形状(start/end 为草稿字符偏移,draftRev 为 CAS 版本)。 */
|
|
101
|
+
export interface TokenSpanLike {
|
|
102
|
+
start: number
|
|
103
|
+
end: number
|
|
104
|
+
draftRev: number
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** ctx 依赖的最小面(sessions.scope + conversation.input)。 */
|
|
108
|
+
export interface CtxLike {
|
|
109
|
+
get: (name: string) => unknown
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 注入器产物:给 EditorView 用的动作集合。 */
|
|
113
|
+
export interface AddToConversation {
|
|
114
|
+
/** 追加文件引用 chip(@path [Lstart-end]);忙态自动降级纯文本。 */
|
|
115
|
+
appendReference(sessionId: string | undefined, path: string, range?: RefRange): Promise<AddOutcome>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 解析会话输入门面;缺失时返回 undefined(动作侧守卫降级)。
|
|
120
|
+
* @author ddj 2026年08月25号
|
|
121
|
+
* @param ctx 客户端服务上下文
|
|
122
|
+
* @param sessionId 会话 id
|
|
123
|
+
* @returns SessionInput 或 undefined
|
|
124
|
+
*/
|
|
125
|
+
export function inputFor(ctx: CtxLike, sessionId: string | undefined): InputLike | undefined {
|
|
126
|
+
if (!sessionId) return undefined
|
|
127
|
+
try {
|
|
128
|
+
const sessions = ctx.get('sessions') as { scope?: (id: string) => unknown } | undefined
|
|
129
|
+
const conversation = ctx.get('conversation') as { input?: { for: (actx: unknown) => unknown } } | undefined
|
|
130
|
+
const actx = sessions?.scope?.(sessionId)
|
|
131
|
+
const shell = actx && conversation?.input?.for?.(actx)
|
|
132
|
+
return (shell && typeof (shell as InputLike).setDraft === 'function') ? shell as InputLike : undefined
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 取当前草稿长度与版本;无输入门面时返回 null。
|
|
139
|
+
* @author ddj 2026年08月25号 */
|
|
140
|
+
function draftCursor(input: InputLike | undefined): { draft: string; draftRev: number } | null {
|
|
141
|
+
try {
|
|
142
|
+
const s = input?.state?.getSnapshot?.()
|
|
143
|
+
if (!s) return null
|
|
144
|
+
return { draft: String(s.draft ?? ''), draftRev: Number(s.draftRev ?? 0) }
|
|
145
|
+
} catch {
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 从 sessions 列表快照读会话工作区目录(与 index.ts 既有读取方式一致)。
|
|
151
|
+
* @author ddj 2026年08月25号 */
|
|
152
|
+
function cwdOf(ctx: CtxLike, sessionId: string | undefined): string | undefined {
|
|
153
|
+
if (!sessionId) return undefined
|
|
154
|
+
try {
|
|
155
|
+
const sessions = ctx.get('sessions') as
|
|
156
|
+
| { list?: { getSnapshot?: () => { byId?: Record<string, { cwd?: string }> } } }
|
|
157
|
+
| undefined
|
|
158
|
+
return sessions?.list?.getSnapshot?.()?.byId?.[sessionId]?.cwd
|
|
159
|
+
} catch {
|
|
160
|
+
return undefined
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 写入成功后的 composer 内联提示(门面可用时;失败静默)。
|
|
165
|
+
* @author ddj 2026年08月25号 */
|
|
166
|
+
function notify(input: InputLike, text: string): void {
|
|
167
|
+
try {
|
|
168
|
+
input.notify?.('info', text)
|
|
169
|
+
} catch {
|
|
170
|
+
/* 提示失败忽略 */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 输入门面插入引用(拦截异常视作未应用,走降级)。
|
|
175
|
+
* @author ddj 2026年08月25号 */
|
|
176
|
+
function safeInsert(input: InputLike, reference: ReferenceInsertLike, span: TokenSpanLike): boolean {
|
|
177
|
+
try {
|
|
178
|
+
return input.insertReference(reference, span) === true
|
|
179
|
+
} catch {
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 创建「添加到对话」动作集(apply 阶段构建一次,随 props 传给 EditorView)。
|
|
186
|
+
* @author ddj 2026年08月25号
|
|
187
|
+
* @param ctx 客户端服务上下文(sessions + conversation)
|
|
188
|
+
* @returns 动作集
|
|
189
|
+
*/
|
|
190
|
+
export function createAddToConversation(ctx: CtxLike): AddToConversation {
|
|
191
|
+
const appendReference: AddToConversation['appendReference'] = async (sessionId, path, range) => {
|
|
192
|
+
const input = inputFor(ctx, sessionId)
|
|
193
|
+
if (!input) return 'unavailable'
|
|
194
|
+
const cur = draftCursor(input)
|
|
195
|
+
if (!cur) return 'unavailable'
|
|
196
|
+
const { reference, mention } = buildFileRef(path, cwdOf(ctx, sessionId), range)
|
|
197
|
+
const span: TokenSpanLike = { start: cur.draft.length, end: cur.draft.length, draftRev: cur.draftRev }
|
|
198
|
+
const ok = safeInsert(input, reference, span)
|
|
199
|
+
if (!ok) {
|
|
200
|
+
// 忙态(adjudicating/submitting)或 CAS 失败:降级纯文本追加,保证动作有落点。
|
|
201
|
+
const gap = cur.draft.length > 0 && !/\s$/.test(cur.draft) ? ' ' : ''
|
|
202
|
+
input.setDraft(cur.draft + gap + mention + ' ')
|
|
203
|
+
notify(input, '已添加文件引用 ' + mention)
|
|
204
|
+
return 'busy'
|
|
205
|
+
}
|
|
206
|
+
notify(input, '已添加文件引用 ' + mention)
|
|
207
|
+
return 'ok'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { appendReference }
|
|
211
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 对话差异 dock 纯函数。
|
|
3
|
+
* 只负责差异文件轮转和文案,状态查询仍由组件复用现有 edrv.list/summarize。
|
|
4
|
+
* 作者 ddj 2026-08-26
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 从稳定差异文件列表中取下一个路径,并返回下一次索引。
|
|
9
|
+
* @author ddj 2026年08月26号
|
|
10
|
+
* @param paths 待处理差异文件路径
|
|
11
|
+
* @param index 当前轮转索引
|
|
12
|
+
* @returns 下一个路径与归一化后的下一索引
|
|
13
|
+
*/
|
|
14
|
+
export function nextDiffPath(paths: string[], index: number): { path: string | null; index: number } {
|
|
15
|
+
if (!paths.length) return { path: null, index: 0 }
|
|
16
|
+
const current = Number.isFinite(index) ? Math.max(0, Math.floor(index)) : 0
|
|
17
|
+
const at = current % paths.length
|
|
18
|
+
return { path: paths[at] ?? null, index: (at + 1) % paths.length }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 生成对话差异 dock 的单按钮文案。
|
|
23
|
+
* @author ddj 2026年08月26号
|
|
24
|
+
* @param count 待处理差异文件数量
|
|
25
|
+
* @returns 展示文案
|
|
26
|
+
*/
|
|
27
|
+
export function diffDockText(count: number): string {
|
|
28
|
+
return '差异 ' + Math.max(0, Math.floor(count)) + ' 个文件 · 查看下一个'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 根据活动文件决定编辑页 dock 形态,加载期间保持 editor 结构稳定。
|
|
33
|
+
* @author ddj 2026年08月26号
|
|
34
|
+
* @param activePath 当前活动文件路径
|
|
35
|
+
* @returns 编辑态或无文件空态
|
|
36
|
+
*/
|
|
37
|
+
export function editorDockMode(activePath: string | null | undefined): 'editor' | 'editor-empty' {
|
|
38
|
+
return activePath ? 'editor' : 'editor-empty'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 获取切换文件期间稳定展示的文件内差异数量。
|
|
43
|
+
* @author ddj 2026年08月26号
|
|
44
|
+
* @param ready 当前文件内容是否已加载完成
|
|
45
|
+
* @param actualTotal 内容就绪后的精确差异数
|
|
46
|
+
* @param fallbackTotal 内容加载期间的文件摘要差异数
|
|
47
|
+
* @returns 非负整数差异数
|
|
48
|
+
*/
|
|
49
|
+
export function displayDiffTotal(ready: boolean, actualTotal: number, fallbackTotal: number): number {
|
|
50
|
+
const value = ready ? actualTotal : fallbackTotal
|
|
51
|
+
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0
|
|
52
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 会话级差异 dock store。
|
|
3
|
+
* EditorView 发布当前编辑上下文,conversation.input.dock 唯一读取并渲染。
|
|
4
|
+
* 作者 ddj 2026年08月26号
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 差异 dock 的显示形态。 */
|
|
8
|
+
export type DiffDockMode = 'chat' | 'editor' | 'editor-empty'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 跨 slot 传递的差异 dock 快照。
|
|
12
|
+
* 操作回调随快照一起更新,避免 dock 持有已卸载编辑器的旧闭包。
|
|
13
|
+
*/
|
|
14
|
+
export interface DiffDockSnapshot {
|
|
15
|
+
mode: DiffDockMode
|
|
16
|
+
[key: string]: unknown
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type DiffDockListener = () => void
|
|
20
|
+
type DiffDockSource = object
|
|
21
|
+
|
|
22
|
+
const values = new Map<string, DiffDockSnapshot>()
|
|
23
|
+
const sources = new Map<string, DiffDockSource>()
|
|
24
|
+
const listeners = new Map<string, Set<DiffDockListener>>()
|
|
25
|
+
|
|
26
|
+
/** 通知指定会话的 dock 订阅者。 */
|
|
27
|
+
function notifyDiff(sessionId: string): void {
|
|
28
|
+
for (const listener of listeners.get(sessionId) ?? []) listener()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 发布指定会话的最新编辑器差异上下文。
|
|
33
|
+
* @author ddj 2026年08月26号
|
|
34
|
+
* @param sessionId 会话 id
|
|
35
|
+
* @param snapshot 完整 DiffBox 操作上下文
|
|
36
|
+
* @param source 发布者令牌,用于卸载时防止误清理新实例
|
|
37
|
+
*/
|
|
38
|
+
export function publishDiffDock(sessionId: string, snapshot: DiffDockSnapshot, source?: DiffDockSource): void {
|
|
39
|
+
values.set(sessionId, snapshot)
|
|
40
|
+
if (source) sources.set(sessionId, source)
|
|
41
|
+
else sources.delete(sessionId)
|
|
42
|
+
notifyDiff(sessionId)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 清除指定会话的差异上下文。
|
|
47
|
+
* @author ddj 2026年08月26号
|
|
48
|
+
* @param sessionId 会话 id
|
|
49
|
+
* @param source 可选发布者令牌;不匹配时忽略清理
|
|
50
|
+
*/
|
|
51
|
+
export function clearDiffDock(sessionId: string, source?: DiffDockSource): void {
|
|
52
|
+
if (source && sources.get(sessionId) !== source) return
|
|
53
|
+
values.delete(sessionId)
|
|
54
|
+
sources.delete(sessionId)
|
|
55
|
+
notifyDiff(sessionId)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 读取指定会话的最新上下文。
|
|
60
|
+
* @author ddj 2026年08月26号
|
|
61
|
+
* @param sessionId 会话 id
|
|
62
|
+
* @returns 最新上下文或 null
|
|
63
|
+
*/
|
|
64
|
+
export function readDiffDock(sessionId?: string): DiffDockSnapshot | null {
|
|
65
|
+
if (!sessionId) return null
|
|
66
|
+
return values.get(sessionId) ?? null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 订阅指定会话的上下文变化。
|
|
71
|
+
* @author ddj 2026年08月26号
|
|
72
|
+
* @param sessionId 会话 id
|
|
73
|
+
* @param listener 变化回调
|
|
74
|
+
* @returns 取消订阅函数
|
|
75
|
+
*/
|
|
76
|
+
export function subscribeDiffDock(sessionId: string, listener: DiffDockListener): () => void {
|
|
77
|
+
let bucket = listeners.get(sessionId)
|
|
78
|
+
if (!bucket) {
|
|
79
|
+
bucket = new Set()
|
|
80
|
+
listeners.set(sessionId, bucket)
|
|
81
|
+
}
|
|
82
|
+
bucket.add(listener)
|
|
83
|
+
return () => {
|
|
84
|
+
bucket!.delete(listener)
|
|
85
|
+
if (!bucket!.size) listeners.delete(sessionId)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 编辑区与原生 composer 的几何边界计算。
|
|
3
|
+
* @author ddj 2026年08月26号
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 计算编辑根节点在当前会话滚动区内、composer 上方可使用的高度。
|
|
8
|
+
* @author ddj 2026年08月26号
|
|
9
|
+
* @param rootTop 编辑根节点顶边
|
|
10
|
+
* @param scrollBottom 会话滚动区可视底边
|
|
11
|
+
* @param composerTop composer 顶边;缺失时回退滚动区底边
|
|
12
|
+
* @returns 非负可用高度
|
|
13
|
+
*/
|
|
14
|
+
export function editorHeight(rootTop: number, scrollBottom: number, composerTop?: number): number {
|
|
15
|
+
const root = Number.isFinite(rootTop) ? rootTop : 0
|
|
16
|
+
const scroll = Number.isFinite(scrollBottom) ? scrollBottom : root
|
|
17
|
+
const composer = Number.isFinite(composerTop) ? composerTop as number : scroll
|
|
18
|
+
return Math.max(0, Math.min(scroll, composer) - root)
|
|
19
|
+
}
|
package/src/client/events.ts
CHANGED
|
@@ -5,18 +5,43 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
* @author ddj 2026年08月
|
|
10
|
-
* @param path 要打开的路径(可空)
|
|
8
|
+
* 选择中央「文件编辑」页签(DOM 级,无需 store actions)。
|
|
9
|
+
* @author ddj 2026年08月26号
|
|
11
10
|
*/
|
|
12
|
-
|
|
11
|
+
function selectEditorTab(): void {
|
|
13
12
|
const tabs = Array.from(document.querySelectorAll('div[role="tablist"] button[role="tab"]'))
|
|
14
|
-
for (const
|
|
15
|
-
if (
|
|
13
|
+
for (const tab of tabs) {
|
|
14
|
+
if (tab.textContent && tab.textContent.includes('文件编辑')) {
|
|
15
|
+
(tab as HTMLButtonElement).click()
|
|
16
|
+
break
|
|
17
|
+
}
|
|
16
18
|
}
|
|
17
|
-
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 打开中央「文件编辑」页签。
|
|
23
|
+
* @author ddj 2026年08月26号
|
|
24
|
+
* @param path 要打开的路径(可空)
|
|
25
|
+
*/
|
|
26
|
+
export function openEditorView(path: string | null): void {
|
|
27
|
+
selectEditorTab()
|
|
28
|
+
const target = path ?? null
|
|
18
29
|
setTimeout(() => {
|
|
19
|
-
window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: { path:
|
|
30
|
+
window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: { path: target } }))
|
|
31
|
+
}, 80)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 打开文件编辑页并聚焦指定文件的首个差异。
|
|
36
|
+
* @author ddj 2026年08月26号
|
|
37
|
+
* @param path 待聚焦的差异文件路径
|
|
38
|
+
*/
|
|
39
|
+
export function openDiffView(path: string): void {
|
|
40
|
+
selectEditorTab()
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
window.dispatchEvent(new CustomEvent('edrv:open-editor', {
|
|
43
|
+
detail: { path, focusDiff: true },
|
|
44
|
+
}))
|
|
20
45
|
}, 80)
|
|
21
46
|
}
|
|
22
47
|
|
|
@@ -30,7 +55,11 @@ export function emitOpenEditor(path: string): void {
|
|
|
30
55
|
window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: { path } }))
|
|
31
56
|
}
|
|
32
57
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
/**
|
|
59
|
+
* 拉起 DiffLauncher。
|
|
60
|
+
* @author ddj 2026年08月26号
|
|
61
|
+
* @param tab 可选的目标页签
|
|
62
|
+
*/
|
|
63
|
+
export function emitShowLauncher(tab?: 'pending' | 'archive'): void {
|
|
64
|
+
window.dispatchEvent(new CustomEvent('edrv:show-launcher', { detail: { tab } }))
|
|
36
65
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 浏览器半入口:slot 注册 + 装配。
|
|
3
|
-
* 挂点:conversation.view「文件编辑」页签(中央 Monaco 编辑器)+ header 差异角标。
|
|
3
|
+
* 挂点:conversation.view「文件编辑」页签(中央 Monaco 编辑器)+ conversation.input.dock 差异条 + header 差异角标。
|
|
4
4
|
* 与 Host 通信:同源 fetch('/edrv/rpc')(shared/rpc 契约)。
|
|
5
5
|
*
|
|
6
6
|
* ⚠️ 跨版本 slot 装配(2026-08-21):新版 DSH 的 slots 系统要求 slot 必须由父 entry
|
|
@@ -8,23 +8,29 @@
|
|
|
8
8
|
* 声明未就绪时抛 `slot ... is not declared (a parent entry's children table must declare it)`。
|
|
9
9
|
* 正确写法 = `ctx.slots.inject(name, () => ctx.slots.register(...))`:声明存在时同步
|
|
10
10
|
* 执行,否则等待声明(官方 ui-conversation 自身即此模式);旧版(rc.8 及更早)同样支持,
|
|
11
|
-
* 故跨版本兼容。slot
|
|
11
|
+
* 故跨版本兼容。slot 名:conversation.view / conversation.session.header.utilities /
|
|
12
|
+
* conversation.input.dock。
|
|
12
13
|
* 作者 ddj 2026-08-20
|
|
13
14
|
*/
|
|
14
15
|
import React from 'react'
|
|
15
16
|
import './styles/editor.css'
|
|
16
17
|
import { EditorView } from './ui/EditorView.js'
|
|
17
18
|
import { DiffBadge } from './ui/DiffBadge.js'
|
|
19
|
+
import { ConversationDiffDock } from './ui/ConversationDiffDock.js'
|
|
18
20
|
import { McpSettings } from './ui/McpSettings.js'
|
|
19
21
|
import { rpc } from './rpc.js'
|
|
22
|
+
import { loadMonaco } from './monaco/loader.js'
|
|
20
23
|
import { createFileOpenerRegistry, scanSidebar, type FileOpenContext } from './fileOpeners.js'
|
|
21
24
|
import type { FileOpenerRegistry } from './fileOpeners.js'
|
|
22
25
|
import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
|
|
23
26
|
import { SettingsContext } from './settingsContext.js'
|
|
24
27
|
import { SIDEBAR_PLUGIN, pickSettingsBinder, registerSlotSafely } from './compat.js'
|
|
28
|
+
import { createAddToConversation } from './addToConversation.js'
|
|
29
|
+
import { createSidebarPanelRegistry } from './sidebar/registry.js'
|
|
30
|
+
import { createFilePanel } from './sidebar/panels/index.js'
|
|
25
31
|
import type { CompatAdapter } from '../shared/compat.js'
|
|
26
32
|
|
|
27
|
-
export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'settingsScope', 'webUiSettings']
|
|
33
|
+
export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'conversation', 'settingsScope', 'webUiSettings']
|
|
28
34
|
|
|
29
35
|
/**
|
|
30
36
|
* 装配客户端:注册中央编辑区视图与 header 差异角标。
|
|
@@ -34,13 +40,36 @@ export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'work
|
|
|
34
40
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
41
|
export function apply(ctx: any): void {
|
|
36
42
|
const schedule = (fn: () => void, ms: number) => ctx.timeout(fn, ms)
|
|
43
|
+
|
|
37
44
|
const registry: FileOpenerRegistry = createFileOpenerRegistry()
|
|
38
45
|
const workspaces = ctx.get('workspaces')
|
|
39
46
|
const sessions = ctx.get('sessions')
|
|
47
|
+
|
|
48
|
+
// Monaco 加载时机:不再 DSH 启动即预热,改为进入会话界面(sessions.list.current 出现)后再后台加载,
|
|
49
|
+
// 用户点开「文件编辑」页签即用;空闲时仍由 EditorView 挂载兜底加载。
|
|
50
|
+
// 模块级 promise 去重,重复触发只首次真正加载;requestIdleCallback 让出会话首屏带宽,缺省回退延时调度;
|
|
51
|
+
// 预热失败静默吞掉(loader 失败会重置 promise,页签打开时仍走原有加载/重试路径)。
|
|
52
|
+
const monacoList = sessions?.list
|
|
53
|
+
if (typeof window !== 'undefined' && monacoList && typeof monacoList.subscribe === 'function') {
|
|
54
|
+
const list = monacoList as { getSnapshot: () => { current?: unknown }; subscribe: (listener: () => void) => () => void }
|
|
55
|
+
const schedulePreload = () => {
|
|
56
|
+
const preload = () => loadMonaco(() => {}).catch(() => {})
|
|
57
|
+
if (typeof window.requestIdleCallback === 'function') window.requestIdleCallback(preload, { timeout: 2000 })
|
|
58
|
+
else schedule(preload, 300)
|
|
59
|
+
}
|
|
60
|
+
const onSessionEnter = () => {
|
|
61
|
+
if (!list.getSnapshot()?.current) return
|
|
62
|
+
schedulePreload()
|
|
63
|
+
}
|
|
64
|
+
ctx.effect(() => list.subscribe(onSessionEnter), 'vscode-mode: monaco session trigger')
|
|
65
|
+
onSessionEnter()
|
|
66
|
+
}
|
|
40
67
|
const originalOpenPath = workspaces?.openPath
|
|
41
68
|
const binder = pickSettingsBinder(ctx)
|
|
42
69
|
const settings = binder.scope
|
|
43
70
|
let selected = autoValue('auto')
|
|
71
|
+
/** 「添加到对话」动作集:编辑区/Tab 右键菜单注入文件引用与代码块(conversation 服务缺失时各动作安全降级)。 */
|
|
72
|
+
const addToConversation = createAddToConversation(ctx)
|
|
44
73
|
|
|
45
74
|
/** 客户端侧兼容摘要(与 host 报告合并展示;惰性求值保证 HMR 后仍新鲜)。 */
|
|
46
75
|
const compatSummary = (): CompatAdapter[] => [
|
|
@@ -51,6 +80,11 @@ export function apply(ctx: any): void {
|
|
|
51
80
|
|
|
52
81
|
ctx.provide('fileOpeners', registry)
|
|
53
82
|
ctx.effect(() => registry.register(vscodeOpener()), 'vscode-mode: file opener')
|
|
83
|
+
|
|
84
|
+
// 侧边栏面板注册表(对外 provide,供本插件/第三方注册面板;文件管理为面板 #1)
|
|
85
|
+
const sidebarPanels = createSidebarPanelRegistry()
|
|
86
|
+
ctx.provide('edrvSidebarPanels', sidebarPanels)
|
|
87
|
+
ctx.effect(() => sidebarPanels.register(createFilePanel()), 'vscode-mode: sidebar panel')
|
|
54
88
|
ctx.effect(() => registry.register({
|
|
55
89
|
id: 'system', label: '系统默认应用', priority: 0,
|
|
56
90
|
open: (path: string) => originalOpenPath.call(workspaces, path),
|
|
@@ -85,14 +119,23 @@ export function apply(ctx: any): void {
|
|
|
85
119
|
}), 'vscode-mode: file link routing')
|
|
86
120
|
}
|
|
87
121
|
|
|
88
|
-
// 中央「文件编辑」页签:类 VSCode
|
|
122
|
+
// 中央「文件编辑」页签:类 VSCode 编辑器(顶部=文件页签+搜索框,左侧=侧边栏,差异 UI=文件底部圆角悬浮框)
|
|
89
123
|
registerSlotSafely(ctx, {
|
|
90
124
|
name: 'conversation.view',
|
|
91
125
|
id: 'edrv-editor',
|
|
92
126
|
order: 5,
|
|
93
127
|
label: '文件编辑',
|
|
94
128
|
inject: (sessionId: string) => ({ sessionId }),
|
|
95
|
-
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { schedule })))
|
|
129
|
+
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { schedule, addToConversation, sidebarPanels })))
|
|
130
|
+
|
|
131
|
+
// 对话输入框上方差异 dock:普通对话显示单文案按钮,文件编辑页由 EditorView 隐藏
|
|
132
|
+
registerSlotSafely(ctx, {
|
|
133
|
+
name: 'conversation.input.dock',
|
|
134
|
+
id: 'edrv-diff-dock',
|
|
135
|
+
order: 30,
|
|
136
|
+
label: '差异',
|
|
137
|
+
inject: (sessionId: string) => ({ sessionId }),
|
|
138
|
+
}, (props: unknown) => React.createElement(ConversationDiffDock, Object.assign({}, props)))
|
|
96
139
|
|
|
97
140
|
// header 差异角标:仅当前工作区存在差异时渲染
|
|
98
141
|
registerSlotSafely(ctx, { name: 'conversation.session.header.utilities', id: 'edrv-diff-badge', order: 90, label: '差异' },
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — 侧边栏容器(活动栏 + 面板区 + 拖拽调宽)。
|
|
4
|
+
* 面板列表来自注册表(活动栏图标/徽标/激活态),渲染当前激活面板的 render(ctx);
|
|
5
|
+
* 右侧手柄拖拽调宽(clamp 180–560),底部按钮收起侧边栏。
|
|
6
|
+
* 作者 ddj 2026-08-26
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import type { SidebarCtx } from './types.js'
|
|
10
|
+
|
|
11
|
+
const W_MIN = 180
|
|
12
|
+
const W_MAX = 560
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 侧边栏容器。
|
|
16
|
+
* @param props.registry 面板注册表(list() 提供面板顺序)
|
|
17
|
+
* @param props.ctx 面板共享上下文
|
|
18
|
+
* @param props.activePanel 当前激活面板 id
|
|
19
|
+
* @param props.onActive 激活面板变化回调
|
|
20
|
+
* @param props.width 面板区宽度
|
|
21
|
+
* @param props.onWidth 宽度变化回调
|
|
22
|
+
* @param props.onHide 收起侧边栏回调
|
|
23
|
+
*/
|
|
24
|
+
export function SidebarView(props) {
|
|
25
|
+
const panels = props.registry?.list?.() ?? []
|
|
26
|
+
const ctx = props.ctx
|
|
27
|
+
const activePanel = props.activePanel
|
|
28
|
+
const onActive = props.onActive
|
|
29
|
+
const width = props.width
|
|
30
|
+
const onWidth = props.onWidth
|
|
31
|
+
const onHide = props.onHide
|
|
32
|
+
|
|
33
|
+
const active = panels.find((p) => p.id === activePanel) || panels[0]
|
|
34
|
+
|
|
35
|
+
const onPointerDown = (e) => {
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
const startX = e.clientX
|
|
38
|
+
const startW = width
|
|
39
|
+
const onMove = (ev) => {
|
|
40
|
+
const w = Math.max(W_MIN, Math.min(W_MAX, startW + (ev.clientX - startX)))
|
|
41
|
+
onWidth(w)
|
|
42
|
+
}
|
|
43
|
+
const onUp = () => {
|
|
44
|
+
window.removeEventListener('pointermove', onMove)
|
|
45
|
+
window.removeEventListener('pointerup', onUp)
|
|
46
|
+
}
|
|
47
|
+
window.addEventListener('pointermove', onMove)
|
|
48
|
+
window.addEventListener('pointerup', onUp)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const rail = React.createElement('div', { className: 'edrv-rail' },
|
|
52
|
+
React.createElement('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', gap: 2 } },
|
|
53
|
+
panels.map((p) => {
|
|
54
|
+
const count = typeof p.badge === 'function' ? p.badge(ctx) : null
|
|
55
|
+
return React.createElement('button', {
|
|
56
|
+
key: p.id,
|
|
57
|
+
className: 'edrv-rail-btn' + (active && p.id === active.id ? ' edrv-rail-on' : ''),
|
|
58
|
+
title: p.title,
|
|
59
|
+
onClick: () => onActive(p.id),
|
|
60
|
+
},
|
|
61
|
+
React.createElement('span', { className: 'edrv-rail-icon' }, p.icon),
|
|
62
|
+
count > 0
|
|
63
|
+
? React.createElement('span', { className: 'edrv-rail-badge' }, String(count))
|
|
64
|
+
: null)
|
|
65
|
+
})),
|
|
66
|
+
React.createElement('button', { className: 'edrv-rail-btn', title: '隐藏侧边栏 (Ctrl+B)', onClick: onHide }, '◀'))
|
|
67
|
+
|
|
68
|
+
return React.createElement('div', { className: 'edrv-sidebar', style: { width } },
|
|
69
|
+
rail,
|
|
70
|
+
React.createElement('div', { className: 'edrv-side-body' },
|
|
71
|
+
active ? active.render(ctx) : null),
|
|
72
|
+
React.createElement('div', { className: 'edrv-side-resize', onPointerDown: onPointerDown }))
|
|
73
|
+
}
|