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
|
@@ -264,6 +264,138 @@ export interface CtxLike {
|
|
|
264
264
|
get: (name: string) => unknown
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
/** 新建会话结果(ok=false 供调用方降级回当前对话注入)。 */
|
|
268
|
+
export interface DraftSessionResult {
|
|
269
|
+
ok: boolean
|
|
270
|
+
/** 新会话 id(ok=true 时存在;投递命令按它内嵌)。 */
|
|
271
|
+
id?: string
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** apiproxy wire 端点(POST /api/<method>;浏览器同源 fetch 自带 cookie 鉴权;实证 401=未鉴权而非 404)。 */
|
|
275
|
+
const SESSION_CREATE_PATH = '/api/session/create'
|
|
276
|
+
/** apiproxy wire 端点:发送任务(session/prompt;点击即发送方案)。 */
|
|
277
|
+
const SESSION_PROMPT_PATH = '/api/session/prompt'
|
|
278
|
+
|
|
279
|
+
/** mint RPC 请求 id(RpcId 品牌类型运行时即普通字符串)。 */
|
|
280
|
+
function mintRpcId(): string {
|
|
281
|
+
try {
|
|
282
|
+
return globalThis.crypto?.randomUUID?.() ?? ('rpc-' + Date.now())
|
|
283
|
+
} catch {
|
|
284
|
+
return 'rpc-' + Date.now()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* RPC 返回解析(照官方 dsh-client-runtime 调用形态:`const { result } = await api.sessions.create(...)`)。
|
|
290
|
+
* 官方 api.sessions 面返回**信封** `{ rpcId, result: RpcResult }`——须解 .result;
|
|
291
|
+
* 兼容裸 RpcResult 形态(个别补丁面直接回 {ok, value})。
|
|
292
|
+
* @author ddj 2026年09月23号
|
|
293
|
+
* @param res 信封或裸 RpcResult
|
|
294
|
+
* @returns 归一后的 { ok, value }
|
|
295
|
+
*/
|
|
296
|
+
function rpcOk(res: unknown): { ok: boolean; value?: unknown } {
|
|
297
|
+
const envelope = (res as { result?: unknown } | null)?.result
|
|
298
|
+
const r = (envelope ?? res) as { ok?: boolean; value?: unknown } | null
|
|
299
|
+
return { ok: r?.ok === true, value: r?.value }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* wire 直调 session/create(主路径):ClientRequest 信封 → ServerResponse.result(RpcResult 形态)。
|
|
304
|
+
* 契约以当前安装版 dsh-host-apiproxy 为准(api/sessions.d.ts「Method signatures are the
|
|
305
|
+
* source of truth」):`create({cwd}) → {sessionId}`;信封 = `{type:'client-request', rpcId,
|
|
306
|
+
* method, payload}` → `{result: {ok:true, value:{sessionId}} | {ok:false, error}}`。
|
|
307
|
+
* @author ddj 2026年09月23号
|
|
308
|
+
* @param cwd 新会话工作区目录
|
|
309
|
+
* @returns 新会话 id(失败空串)
|
|
310
|
+
*/
|
|
311
|
+
async function apiSessionCreate(cwd: string | undefined): Promise<string> {
|
|
312
|
+
const res = await fetch(SESSION_CREATE_PATH, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
headers: { 'content-type': 'application/json' },
|
|
315
|
+
body: JSON.stringify({
|
|
316
|
+
type: 'client-request',
|
|
317
|
+
rpcId: mintRpcId(),
|
|
318
|
+
method: 'session/create',
|
|
319
|
+
payload: cwd ? { cwd } : {},
|
|
320
|
+
}),
|
|
321
|
+
})
|
|
322
|
+
const json = await res.json().catch(() => null) as { result?: { ok?: boolean; value?: { sessionId?: unknown } } } | null
|
|
323
|
+
const id = json?.result?.value?.sessionId
|
|
324
|
+
return json?.result?.ok === true && typeof id === 'string' ? id : ''
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* wire 直发 session/prompt(点击即发送方案):任务文本投给目标会话 agent。
|
|
329
|
+
* 契约 = 当前安装树 SessionsApi.prompt:`{sessionId, mode:'queue', content:[{type:'text',text}]}`。
|
|
330
|
+
* 发送成功会话即有内容 → 不再被 blank 隐藏(列表可见)。
|
|
331
|
+
* @author ddj 2026年09月23号
|
|
332
|
+
* @param sessionId 目标会话 id(新会话)
|
|
333
|
+
* @param text 任务文本
|
|
334
|
+
* @returns 是否受理
|
|
335
|
+
*/
|
|
336
|
+
export async function apiSessionPrompt(sessionId: string, text: string): Promise<boolean> {
|
|
337
|
+
try {
|
|
338
|
+
const res = await fetch(SESSION_PROMPT_PATH, {
|
|
339
|
+
method: 'POST',
|
|
340
|
+
headers: { 'content-type': 'application/json' },
|
|
341
|
+
body: JSON.stringify({
|
|
342
|
+
type: 'client-request',
|
|
343
|
+
rpcId: mintRpcId(),
|
|
344
|
+
method: 'session/prompt',
|
|
345
|
+
payload: { sessionId, mode: 'queue', content: [{ type: 'text', text }] },
|
|
346
|
+
}),
|
|
347
|
+
})
|
|
348
|
+
const json = await res.json().catch(() => null) as { result?: { ok?: boolean } } | null
|
|
349
|
+
return json?.result?.ok === true
|
|
350
|
+
} catch {
|
|
351
|
+
return false
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 新建独立会话承载旁路任务(02-deep-session-prompt:深度分析不污染当前对话)。
|
|
357
|
+
* 主路径 = apiproxy wire 直调(不依赖 service 面形态猜测);回落 remote.session.create
|
|
358
|
+
* 与旧 sessions.create 两代 service 面(宽松解析)。任一步不符返回 ok:false,调用方降级。
|
|
359
|
+
* @author ddj 2026年09月23号
|
|
360
|
+
* @param ctx 客户端服务上下文(remote.session + sessions,仅回落用)
|
|
361
|
+
* @param cwd 新会话工作区目录(与当前会话同工作区)
|
|
362
|
+
* @returns 新会话结果(含 id)
|
|
363
|
+
*/
|
|
364
|
+
export async function newDraftSession(ctx: CtxLike, cwd: string | undefined): Promise<DraftSessionResult> {
|
|
365
|
+
// 主路径:wire 直调
|
|
366
|
+
try {
|
|
367
|
+
const id = await apiSessionCreate(cwd)
|
|
368
|
+
if (id) return { ok: true, id }
|
|
369
|
+
} catch { /* wire 不可用:回落 service 面 */ }
|
|
370
|
+
try {
|
|
371
|
+
// 回落 1:remote.session service 面(官方返回信封 {rpcId, result},见 rpcOk;实参双形态兼容)
|
|
372
|
+
const remote = ctx.get('remote.session') as {
|
|
373
|
+
create?: (req: unknown) => Promise<unknown>
|
|
374
|
+
} | undefined
|
|
375
|
+
if (typeof remote?.create === 'function') {
|
|
376
|
+
const payload = cwd ? { cwd } : {}
|
|
377
|
+
for (const arg of [payload, { rpcId: mintRpcId(), payload }]) {
|
|
378
|
+
const r = rpcOk(await remote.create(arg))
|
|
379
|
+
const id = r.value as { sessionId?: unknown } | undefined
|
|
380
|
+
if (r.ok && typeof id?.sessionId === 'string') return { ok: true, id: id.sessionId }
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// 回落 2:旧版 client store 形态(id / {id} / {sessionId} / {session:{id}} 四解)
|
|
384
|
+
const sessions = ctx.get('sessions') as { create?: (opts?: { cwd?: string }) => unknown } | undefined
|
|
385
|
+
if (typeof sessions?.create === 'function') {
|
|
386
|
+
const created = await sessions.create(cwd ? { cwd } : undefined)
|
|
387
|
+
const id = typeof created === 'string' && created ? created
|
|
388
|
+
: typeof (created as { id?: unknown } | null)?.id === 'string' ? (created as { id: string }).id
|
|
389
|
+
: typeof (created as { sessionId?: unknown } | null)?.sessionId === 'string' ? (created as { sessionId: string }).sessionId
|
|
390
|
+
: typeof (created as { session?: { id?: unknown } } | null)?.session?.id === 'string'
|
|
391
|
+
? (created as { session: { id: string } }).session.id
|
|
392
|
+
: ''
|
|
393
|
+
if (id) return { ok: true, id }
|
|
394
|
+
}
|
|
395
|
+
} catch { /* 全部失败:调用方降级 */ }
|
|
396
|
+
return { ok: false }
|
|
397
|
+
}
|
|
398
|
+
|
|
267
399
|
/** 注入器产物:给 EditorView 用的动作集合。 */
|
|
268
400
|
export interface AddToConversation {
|
|
269
401
|
/** 追加文件/文件夹引用 chip(@path [Lstart-end]);忙态自动降级纯文本。 */
|
|
@@ -273,6 +405,12 @@ export interface AddToConversation {
|
|
|
273
405
|
range?: RefRange,
|
|
274
406
|
appearance?: RefAppearance,
|
|
275
407
|
): Promise<AddOutcome>
|
|
408
|
+
/** 追加纯文本(AI 深度分析任务等长文本注入;插到当前光标,失败不整篇重建)。 */
|
|
409
|
+
appendText(sessionId: string | undefined, text: string): Promise<AddOutcome>
|
|
410
|
+
/** 新建独立会话并切换(旁路任务隔离;失败 ok:false 供降级回当前对话)。 */
|
|
411
|
+
startDraftSession(cwd?: string): Promise<DraftSessionResult>
|
|
412
|
+
/** 发送任务给目标会话 agent(点击即发送;失败 false 供降级)。 */
|
|
413
|
+
sendTask(sessionId: string | undefined, text: string): Promise<boolean>
|
|
276
414
|
}
|
|
277
415
|
|
|
278
416
|
/**
|
|
@@ -392,5 +530,69 @@ export function createAddToConversation(ctx: CtxLike): AddToConversation {
|
|
|
392
530
|
return 'failed'
|
|
393
531
|
}
|
|
394
532
|
|
|
395
|
-
|
|
533
|
+
/**
|
|
534
|
+
* 追加纯文本(AI 深度分析任务注入):走官方 plain-text 通道插到当前光标。
|
|
535
|
+
* 失败语义与 appendReference 一致:宁可不写也不整篇重建(保护既有 chip)。
|
|
536
|
+
* @author ddj 2026年09月23号
|
|
537
|
+
* @param sessionId 会话 id
|
|
538
|
+
* @param text 待注入文本
|
|
539
|
+
* @returns 注入结果(ok / unavailable / failed)
|
|
540
|
+
*/
|
|
541
|
+
const appendText: AddToConversation['appendText'] = async (sessionId, text) => {
|
|
542
|
+
const input = inputFor(ctx, sessionId)
|
|
543
|
+
if (!input) return 'unavailable'
|
|
544
|
+
const cur = draftCursor(input)
|
|
545
|
+
if (!cur) return 'unavailable'
|
|
546
|
+
const at = insertOffsetOf(caretOf(input), detectEndOf(cur))
|
|
547
|
+
const span: TokenSpanLike = { start: at, end: at, draftRev: cur.draftRev }
|
|
548
|
+
return safeInsertText(input, text, span) ? 'ok' : 'failed'
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* 新建独立会话(02-deep-session-prompt:旁路任务不污染当前对话)。
|
|
553
|
+
* 闭包持有真服务 ctx(面板 SidebarCtx 无 .get,会话操作必须经此门面);
|
|
554
|
+
* 创建后尽力 `sessions.open(id)` 切换视图(缺失不致命,草稿仍可写入)。
|
|
555
|
+
* @author ddj 2026年09月23号
|
|
556
|
+
* @param cwd 新会话工作区目录(与当前会话同工作区)
|
|
557
|
+
* @returns 新会话结果(失败供降级)
|
|
558
|
+
*/
|
|
559
|
+
const startDraftSession: AddToConversation['startDraftSession'] = async (cwd) => {
|
|
560
|
+
const result = await newDraftSession(ctx, cwd)
|
|
561
|
+
if (result.ok && result.id) {
|
|
562
|
+
// 尽力切换到新会话(缺失/异常不影响结果——仅视图不切换)
|
|
563
|
+
try {
|
|
564
|
+
const sessions = ctx.get('sessions') as { open?: (id: string) => void } | undefined
|
|
565
|
+
sessions?.open?.(result.id)
|
|
566
|
+
} catch { /* 切换失败仅视图停留 */ }
|
|
567
|
+
}
|
|
568
|
+
return result
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* 发送任务给目标会话 agent(点击即发送方案:任务落地会话即非 blank,列表可见)。
|
|
573
|
+
* 主路径 wire 直发 session/prompt;回落 remote.session.prompt service 面。
|
|
574
|
+
* @author ddj 2026年09月23号
|
|
575
|
+
* @param sessionId 目标会话 id
|
|
576
|
+
* @param text 任务文本
|
|
577
|
+
* @returns 是否受理
|
|
578
|
+
*/
|
|
579
|
+
const sendTask: AddToConversation['sendTask'] = async (sessionId, text) => {
|
|
580
|
+
if (!sessionId) return false
|
|
581
|
+
try {
|
|
582
|
+
if (await apiSessionPrompt(sessionId, text)) return true
|
|
583
|
+
} catch { /* 回落 service 面 */ }
|
|
584
|
+
try {
|
|
585
|
+
const remote = ctx.get('remote.session') as { prompt?: (req: unknown) => Promise<unknown> } | undefined
|
|
586
|
+
if (typeof remote?.prompt === 'function') {
|
|
587
|
+
// 官方 api.sessions.prompt(request) 返回信封(rpcOk 解 .result);实参双形态兼容
|
|
588
|
+
const payload = { sessionId, mode: 'queue', content: [{ type: 'text', text }] }
|
|
589
|
+
for (const arg of [payload, { rpcId: mintRpcId(), payload }]) {
|
|
590
|
+
if (rpcOk(await remote.prompt(arg)).ok) return true
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
} catch { /* 全失败 */ }
|
|
594
|
+
return false
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return { appendReference, appendText, startDraftSession, sendTask }
|
|
396
598
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 跨挂载/跨热重载的待跳转交接(pending nav handoff)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须落 window(第三次事故根因,2026-09-23 实测取证):
|
|
5
|
+
* 并行改码触发插件 client HMR 热重载(保留 console 中装配日志重复 6-7 轮为证)时,
|
|
6
|
+
* EditorView 被重挂载、模块级变量随 bundle 重求值复位——挂载级 `pendingFocusRef`
|
|
7
|
+
* 一并销毁,首开跳转只打开文件、不落目标行,热重载平息后的第二次点击才正常。
|
|
8
|
+
* window 属性跨热重载存活(先例:`__edrvLspProvidersRegistered__`、`__DSH_DEBUG_LOG__`)。
|
|
9
|
+
*
|
|
10
|
+
* 语义约定:
|
|
11
|
+
* - `putPendingNav` 写槽(openFileAt 与挂载级 ref 同步写,ref 是快路径、槽是交接路径);
|
|
12
|
+
* - `readPendingNav` **只读不消费**——过期/形状非法才清;新鲜槽可反复读;
|
|
13
|
+
* - 落点成功后由调用方 `clearPendingNav`(与 ref 同一清除点,防止残留二次跳);
|
|
14
|
+
* - TTL 到点自动失效,避免"稍后莫名跳一下"(用户已离开目标意图窗口)。
|
|
15
|
+
* 作者 ddj 2026-09-23
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// --region 类型与常量
|
|
19
|
+
|
|
20
|
+
/** 待跳转形状(行列均 1-based;at = 写入时刻,TTL 判据)。 */
|
|
21
|
+
export interface PendingNav {
|
|
22
|
+
path: string
|
|
23
|
+
line?: number | null
|
|
24
|
+
column?: number | null
|
|
25
|
+
endLine?: number | null
|
|
26
|
+
endColumn?: number | null
|
|
27
|
+
at: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 写入接口(at 由 putPendingNav 盖时间戳)。 */
|
|
31
|
+
export type PendingNavInput = Omit<PendingNav, 'at'>
|
|
32
|
+
|
|
33
|
+
/** window 槽键名(跨热重载存活的全局挂点)。 */
|
|
34
|
+
export const PENDING_NAV_KEY = '__edrvPendingNav__'
|
|
35
|
+
|
|
36
|
+
/** 交接有效期:超过即失效清槽(点击意图的有效窗口,防陈旧跳转)。 */
|
|
37
|
+
export const PENDING_NAV_TTL = 30000
|
|
38
|
+
|
|
39
|
+
// --endregion
|
|
40
|
+
|
|
41
|
+
// --region 纯函数(可单测)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 形状守卫:非法入参返回 null,合法则归一化为 PendingNav。
|
|
45
|
+
* 行列非有限数字一律归 null(缺省语义 = 不定位行),path 必须为非空字符串。
|
|
46
|
+
* @author ddj 2026年09月23号
|
|
47
|
+
* @param raw 任意来源的原始值
|
|
48
|
+
* @returns 归一化后的 PendingNav 或 null
|
|
49
|
+
*/
|
|
50
|
+
export function sanitizeNav(raw: unknown): PendingNav | null {
|
|
51
|
+
if (!raw || typeof raw !== 'object') return null
|
|
52
|
+
const src = raw as Record<string, unknown>
|
|
53
|
+
if (typeof src.path !== 'string' || !src.path) return null
|
|
54
|
+
const num = (v: unknown): number | null => (typeof v === 'number' && Number.isFinite(v) ? v : null)
|
|
55
|
+
const at = num(src.at)
|
|
56
|
+
if (at === null) return null // at 是 TTL 判据:缺失/非法直接整体无效,不兜 0 免依赖巧合
|
|
57
|
+
return {
|
|
58
|
+
path: src.path,
|
|
59
|
+
line: num(src.line),
|
|
60
|
+
column: num(src.column),
|
|
61
|
+
endLine: num(src.endLine),
|
|
62
|
+
endColumn: num(src.endColumn),
|
|
63
|
+
at,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 新鲜度判定:形状合法且未超过 TTL 才算有效;过期返回 null。
|
|
69
|
+
* @author ddj 2026年09月23号
|
|
70
|
+
* @param nav 已归一化的待跳转(可空)
|
|
71
|
+
* @param now 当前时刻(毫秒)
|
|
72
|
+
* @param ttl 有效期(毫秒)
|
|
73
|
+
* @returns 有效导航或 null
|
|
74
|
+
*/
|
|
75
|
+
export function freshNav(nav: PendingNav | null, now: number, ttl: number): PendingNav | null {
|
|
76
|
+
if (!nav) return null
|
|
77
|
+
return now - nav.at <= ttl ? nav : null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --endregion
|
|
81
|
+
|
|
82
|
+
// --region window 槽适配
|
|
83
|
+
|
|
84
|
+
/** window 上的槽宿主(node 环境无 window,读写降级为 no-op)。 */
|
|
85
|
+
type NavSlotHost = Record<string, unknown>
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 取槽宿主:仅浏览器环境返回 window,否则 undefined(测试/SSR 降级)。
|
|
89
|
+
* @author ddj 2026年09月23号
|
|
90
|
+
*/
|
|
91
|
+
function slotHost(): NavSlotHost | undefined {
|
|
92
|
+
if (typeof window === 'undefined') return undefined
|
|
93
|
+
return window as unknown as NavSlotHost
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 写入待跳转(盖写整槽;at 由调用方给定或取当前时间)。
|
|
98
|
+
* 非法 path 直接丢弃,不写槽。
|
|
99
|
+
* @author ddj 2026年09月23号
|
|
100
|
+
* @param nav 待跳转内容(不含 at)
|
|
101
|
+
* @param now 写入时刻(测试注入用)
|
|
102
|
+
* @returns 是否写入成功
|
|
103
|
+
*/
|
|
104
|
+
export function putPendingNav(nav: PendingNavInput, now: number = Date.now()): boolean {
|
|
105
|
+
const host = slotHost()
|
|
106
|
+
if (!host) return false
|
|
107
|
+
if (typeof nav?.path !== 'string' || !nav.path) return false
|
|
108
|
+
host[PENDING_NAV_KEY] = { ...nav, at: now }
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 读取新鲜的待跳转(**不消费**;过期或形状非法才清槽)。
|
|
114
|
+
* @author ddj 2026年09月23号
|
|
115
|
+
* @param now 当前时刻(测试注入用)
|
|
116
|
+
* @param ttl 有效期(毫秒)
|
|
117
|
+
* @returns 有效导航或 null
|
|
118
|
+
*/
|
|
119
|
+
export function readPendingNav(now: number = Date.now(), ttl: number = PENDING_NAV_TTL): PendingNav | null {
|
|
120
|
+
const host = slotHost()
|
|
121
|
+
if (!host) return null
|
|
122
|
+
const nav = freshNav(sanitizeNav(host[PENDING_NAV_KEY]), now, ttl)
|
|
123
|
+
if (!nav) {
|
|
124
|
+
// 失效即清:过期/垃圾数据不留在 window 上
|
|
125
|
+
if (host[PENDING_NAV_KEY] !== undefined) delete host[PENDING_NAV_KEY]
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
return nav
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 清除待跳转槽(落点成功或显式放弃时调用)。
|
|
133
|
+
* @author ddj 2026年09月23号
|
|
134
|
+
*/
|
|
135
|
+
export function clearPendingNav(): void {
|
|
136
|
+
const host = slotHost()
|
|
137
|
+
if (!host) return
|
|
138
|
+
delete host[PENDING_NAV_KEY]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --endregion
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 作者 ddj 2026-08-26 / 2026-08-27 / 2026-08-31 / 2026-09-10 / 2026-09-16
|
|
11
11
|
*/
|
|
12
12
|
import React from 'react'
|
|
13
|
-
import {
|
|
13
|
+
import { IconFolderClose16, IconFolderOpen16, IconRefreshOutline16, fileIconEl } from '../../ui/icons.js'
|
|
14
14
|
import { rpc } from '../../rpc.js'
|
|
15
15
|
import { ContextMenu } from '../../ui/ContextMenu.js'
|
|
16
16
|
import { buildTreeMenu } from '../contextMenu.js'
|
|
@@ -48,20 +48,7 @@ function dirIconEl(isOpen) {
|
|
|
48
48
|
return React.createElement(Icon, { size: 16, className: 'edrv-tree-icon' })
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
* 文件图标元素(官方 FileTypeIcon + classifyFileType 分类,未知类型为 other)。
|
|
53
|
-
* @author ddj 2026年09月10号
|
|
54
|
-
* @param name 文件名(含扩展名)
|
|
55
|
-
* @returns 图标元素;原语缺失或分类异常返回 null
|
|
56
|
-
*/
|
|
57
|
-
function fileIconEl(name) {
|
|
58
|
-
if (typeof FileTypeIcon !== 'function' || typeof classifyFileType !== 'function') return null
|
|
59
|
-
try {
|
|
60
|
-
return React.createElement(FileTypeIcon, { kind: classifyFileType(String(name ?? '')), size: 16, className: 'edrv-tree-icon' })
|
|
61
|
-
} catch (error) {
|
|
62
|
-
return null
|
|
63
|
-
}
|
|
64
|
-
}
|
|
51
|
+
// 文件图标元素(fileIconEl)已上移 icons.ts 共享(SVN 变更行同用),此处直接引用。
|
|
65
52
|
|
|
66
53
|
/**
|
|
67
54
|
* 行图标分派:目录 → 文件夹图标,文件 → 官方类型图标。
|