dsh-vscode-mode 0.25.1 → 0.27.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 +25 -11
- package/lib/client.js +1969 -1344
- package/lib/client.js.map +2 -2
- package/lib/index.js +1443 -716
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compatSummary.ts +4 -0
- package/src/client/contextMenusOfficial.ts +80 -0
- package/src/client/forgeBridge.ts +28 -0
- package/src/client/index.ts +18 -3
- package/src/client/monaco/lsp/lspClient.ts +25 -7
- package/src/client/officialSidebar.ts +15 -3
- package/src/client/reviewBar.ts +3 -0
- package/src/client/reviewBatch.ts +9 -1
- package/src/client/reviewKeepAll.ts +72 -0
- package/src/client/sidebar/menuItems.ts +6 -0
- package/src/client/sidebar/panels/FileExplorer.ts +26 -1
- package/src/client/sidebar/types.ts +3 -0
- package/src/client/state/diffSummary.ts +34 -7
- package/src/client/state/recordsFeed.ts +40 -3
- package/src/client/state/reviewFeed.ts +9 -2
- package/src/client/tabMenu.ts +5 -0
- package/src/client/ui/ClaimRouter.ts +11 -5
- package/src/client/ui/EditorPane.ts +9 -1
- package/src/client/ui/EditorView.ts +164 -39
- package/src/client/ui/OfficialSideTab.ts +13 -5
- package/src/client/ui/ReviewAllConfirm.ts +1 -1
- package/src/index.ts +1 -1
- package/src/pending/capture.ts +96 -47
- package/src/pending/persist.ts +94 -5
- package/src/pending/store.ts +171 -58
- package/src/pendingCompat.ts +72 -9
- package/src/pendingRpc.ts +10 -10
- package/src/reviewCapture.ts +64 -23
- package/src/reviewDb.ts +119 -8
- package/src/reviewEvidence.ts +1 -1
- package/src/reviewRpc.ts +115 -9
- package/src/rpc.ts +14 -10
- package/src/shared/review.ts +21 -0
- package/src/shared/rpc.ts +3 -1
- package/src/store.ts +44 -0
- package/src/toolTargets.ts +63 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -13,6 +13,8 @@ export interface ClientCompatState {
|
|
|
13
13
|
fileClaim: boolean
|
|
14
14
|
claimWanted: boolean
|
|
15
15
|
selected: string
|
|
16
|
+
officialMenus?: boolean
|
|
17
|
+
forge?: boolean
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -73,5 +75,7 @@ export function clientSummary(state: ClientCompatState, version = ''): CompatAda
|
|
|
73
75
|
if (state.fileClaim || (state.claimWanted && (official || officialExpected !== false))) {
|
|
74
76
|
items.push({ name: '文件链接官方认领(dsh-resource://file)', active: state.fileClaim, note: state.fileClaim ? '聊天文件链接转发进单一编辑器页签' : '官方文件链接认领未安装' })
|
|
75
77
|
}
|
|
78
|
+
if (state.officialMenus !== undefined) items.push({ name: '官方右键菜单', active: state.officialMenus, note: state.officialMenus ? '插件动作接入共享菜单' : '服务缺失或装配失败,保留插件菜单' })
|
|
79
|
+
if (state.forge !== undefined) items.push({ name: 'Forge 导航', active: state.forge, note: state.forge ? '导航至官方面板与本地变更;PR 保持官方审查' : '官方 Forge 未安装或未启用,不自动启用' })
|
|
76
80
|
return items
|
|
77
81
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Optional shared popup adapter retaining the plugin's action models. @author ddj 2026年11月04号 */
|
|
2
|
+
export interface PopupItem {
|
|
3
|
+
id: string
|
|
4
|
+
label: string
|
|
5
|
+
disabled?: boolean
|
|
6
|
+
danger?: boolean
|
|
7
|
+
separator?: boolean
|
|
8
|
+
commandId?: string
|
|
9
|
+
run: () => void | Promise<void>
|
|
10
|
+
}
|
|
11
|
+
interface MenuTarget { kind: string; value: { owner: string; sessionId: string; path: string } }
|
|
12
|
+
interface MenuService {
|
|
13
|
+
register(contribution: { id: string; target: string; order: number; resolve: (target: MenuTarget['value'], signal: AbortSignal) => unknown[] }): () => void
|
|
14
|
+
open(target: MenuTarget, point: { x: number; y: number }): void
|
|
15
|
+
close(): void
|
|
16
|
+
active?: { getSnapshot(): { kind?: string; value?: { owner?: string } } | null }
|
|
17
|
+
}
|
|
18
|
+
export interface OfficialMenuBridge {
|
|
19
|
+
open(kind: 'resource' | 'tab', sessionId: string, path: string, point: { x: number; y: number }, items: PopupItem[], valid: () => boolean): boolean
|
|
20
|
+
invalidate(valid: () => boolean): void
|
|
21
|
+
dispose(): void
|
|
22
|
+
}
|
|
23
|
+
let ownerSeq = 0
|
|
24
|
+
|
|
25
|
+
/** @public @author ddj 2026年11月04号 @param ctx Optional service context. @returns Retry-on-use bridge for late service registration, with legacy fallback. */
|
|
26
|
+
export function lazyMenus(ctx: { get(name: string): unknown }): OfficialMenuBridge {
|
|
27
|
+
let bridge: OfficialMenuBridge | undefined
|
|
28
|
+
let stopped = false
|
|
29
|
+
return {
|
|
30
|
+
open(...args) { if (stopped) return false; bridge ??= officialMenus(ctx); return bridge?.open(...args) ?? false },
|
|
31
|
+
invalidate(valid) { bridge?.invalidate(valid) },
|
|
32
|
+
dispose() { stopped = true; bridge?.dispose(); bridge = undefined },
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @public @author ddj 2026年11月04号 @param ctx Optional service context. @returns Scoped bridge or no capability, preserving the existing popup. */
|
|
37
|
+
export function officialMenus(ctx: { get(name: string): unknown }): OfficialMenuBridge | undefined {
|
|
38
|
+
let service: MenuService
|
|
39
|
+
try { service = ctx.get('contextMenus') as MenuService } catch { return undefined }
|
|
40
|
+
if (!service || typeof service.register !== 'function' || typeof service.open !== 'function' || typeof service.close !== 'function') return undefined
|
|
41
|
+
const owner = 'edrv-menu-' + ++ownerSeq
|
|
42
|
+
let current: { items: PopupItem[]; valid: () => boolean; revision: number } | undefined
|
|
43
|
+
let revision = 0
|
|
44
|
+
let stopped = false
|
|
45
|
+
const disposers: Array<() => void> = []
|
|
46
|
+
try {
|
|
47
|
+
for (const kind of ['resource', 'tab']) {
|
|
48
|
+
disposers.push(service.register({ id: owner + '-' + kind, target: 'edrv.' + kind, order: 30,
|
|
49
|
+
resolve: (target, signal) => {
|
|
50
|
+
const opening = current
|
|
51
|
+
if (stopped || target.owner !== owner || !opening || !opening.valid() || signal.aborted) return []
|
|
52
|
+
return opening.items.map((item) => ({ id: item.id, label: item.label, disabled: item.disabled,
|
|
53
|
+
danger: item.danger, separatorBefore: item.separator, commandId: item.commandId,
|
|
54
|
+
run: async () => {
|
|
55
|
+
if (stopped || opening.revision !== revision || !opening.valid() || item.disabled) return
|
|
56
|
+
await item.run()
|
|
57
|
+
} }))
|
|
58
|
+
},
|
|
59
|
+
}))
|
|
60
|
+
}
|
|
61
|
+
} catch { for (const dispose of disposers) dispose(); return undefined }
|
|
62
|
+
/** @private @author ddj 2026年11月04号 Close only this bridge's active shared popup. */
|
|
63
|
+
function closeOwned(): void {
|
|
64
|
+
const active = service.active?.getSnapshot()
|
|
65
|
+
if (active?.value?.owner === owner && (active.kind === 'edrv.resource' || active.kind === 'edrv.tab')) service.close()
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
open(kind, sessionId, path, point, items, valid) {
|
|
69
|
+
if (stopped || !sessionId || !valid()) return false
|
|
70
|
+
current = { items, valid, revision: ++revision }
|
|
71
|
+
try { service.open({ kind: 'edrv.' + kind, value: { owner, sessionId, path } }, point); return true }
|
|
72
|
+
catch { current = undefined; return false }
|
|
73
|
+
},
|
|
74
|
+
invalidate(valid) {
|
|
75
|
+
if (current?.valid !== valid) return
|
|
76
|
+
revision++; current = undefined; closeOwned()
|
|
77
|
+
},
|
|
78
|
+
dispose() { stopped = true; revision++; current = undefined; closeOwned(); for (const dispose of disposers) dispose() },
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Optional navigation only; remote snapshots never become local editable files. @author ddj 2026年11月04号 */
|
|
2
|
+
export interface ForgeBridge { available(): boolean; open(sessionId: string, local?: boolean): boolean }
|
|
3
|
+
|
|
4
|
+
/** @public @author ddj 2026年11月04号 @param ctx Client capabilities. @param current Current session reader. @returns Capability-gated official navigation, with no polling or writes. */
|
|
5
|
+
export function forgeBridge(ctx: { get(name: string): unknown }, current: () => string | undefined): ForgeBridge {
|
|
6
|
+
const capabilities = () => {
|
|
7
|
+
try {
|
|
8
|
+
const tabs = ctx.get('sidebarRightTabs') as { get?(kind: string): unknown }
|
|
9
|
+
const service = ctx.get('sidebarRight') as { openTab?(kind: string): void; openResource?(address: string, options?: unknown): void }
|
|
10
|
+
return { tabs, service }
|
|
11
|
+
} catch { return undefined }
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
available() {
|
|
15
|
+
const capabilitiesNow = capabilities()
|
|
16
|
+
return Boolean(capabilitiesNow?.tabs?.get?.('forge-panel') && capabilitiesNow.tabs.get('forge-changes') && capabilitiesNow.service?.openTab && capabilitiesNow.service.openResource)
|
|
17
|
+
},
|
|
18
|
+
open(sessionId, local = false) {
|
|
19
|
+
if (!sessionId || current() !== sessionId || !this.available()) return false
|
|
20
|
+
const service = capabilities()!.service
|
|
21
|
+
try {
|
|
22
|
+
if (local) service.openResource!('dsh-resource://forge-changes/' + encodeURIComponent(sessionId) + '/local', { kind: 'forge-changes' })
|
|
23
|
+
else service.openTab!('forge-panel')
|
|
24
|
+
return true
|
|
25
|
+
} catch { return false }
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -22,6 +22,9 @@ import { PerfSizeBadge } from './ui/PerfSizeBadge.js'
|
|
|
22
22
|
import { ConversationDiffDock } from './ui/ConversationDiffDock.js'
|
|
23
23
|
import { McpSettings } from './ui/McpSettings.js'
|
|
24
24
|
import { mcpSessionState } from './mcpSessionState.js'
|
|
25
|
+
import { forgeBridge } from './forgeBridge.js'
|
|
26
|
+
import { lazyMenus } from './contextMenusOfficial.js'
|
|
27
|
+
import { IconCodeOutline16 } from './ui/icons.js'
|
|
25
28
|
import { McpSessionIcon, McpSessionDetail, type McpRowProps } from './ui/McpSessionStatus.js'
|
|
26
29
|
import { PluginDetailSection } from './ui/PluginDetailSection.js'
|
|
27
30
|
import { rpc } from './rpc.js'
|
|
@@ -442,6 +445,7 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
442
445
|
sidebarOpener: registry.get(SIDEBAR_PLUGIN) !== undefined,
|
|
443
446
|
legacySidebar: sideService !== undefined, officialSidebar: officialService !== undefined,
|
|
444
447
|
fileClaim: claimDisposer !== null, claimWanted: shouldClaimFiles(selected), selected,
|
|
448
|
+
officialMenus: Boolean(ctx.get('contextMenus')), forge: forge.available(),
|
|
445
449
|
}, version)
|
|
446
450
|
|
|
447
451
|
ctx.provide('fileOpeners', registry)
|
|
@@ -484,7 +488,10 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
484
488
|
return sidebar ? registry.register(sidebar) : undefined
|
|
485
489
|
}, 'vscode-mode: sidebar file opener')
|
|
486
490
|
// 官方侧边栏正文组件装配(页类型正文;兼作 file 认领转发失败时的兜底正文)
|
|
487
|
-
const
|
|
491
|
+
const forge = forgeBridge(ctx, () => readSessionScope(ctx).sessionId ?? undefined)
|
|
492
|
+
const menuBridge = lazyMenus(ctx)
|
|
493
|
+
if (menuBridge) ctx.effect(() => () => menuBridge.dispose(), 'vscode-mode: official menus')
|
|
494
|
+
const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, officialMenus: menuBridge, forge, fileOpeners: registry, sessions, readSessionScope: () => readSessionScope(ctx) }))
|
|
488
495
|
/** 官方 file 地址认领同步:自动/VSCodeMode 档认领(转发进单一编辑器页签),其余交官方查看器。 */
|
|
489
496
|
const syncFileClaim = (): void => {
|
|
490
497
|
const official = officialService
|
|
@@ -669,7 +676,7 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
669
676
|
order: 5,
|
|
670
677
|
label: () => t('slot.fileEditor'),
|
|
671
678
|
inject: (sessionId: string) => ({ sessionId }),
|
|
672
|
-
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions })))
|
|
679
|
+
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, officialMenus: menuBridge, forge, fileOpeners: registry, sessions })))
|
|
673
680
|
}
|
|
674
681
|
|
|
675
682
|
// 侧边栏三形态互斥切换:官方(DSH 0.1.5+)> better-sidebar(归档,仅旧版回退)> 中央页签。
|
|
@@ -748,7 +755,7 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
748
755
|
if (legacyDisposer !== null) { legacyDisposer(); legacyDisposer = null }
|
|
749
756
|
sideDisposer = ctx.effect(() => installSideEditor({
|
|
750
757
|
service: sideService as NonNullable<typeof sideService>,
|
|
751
|
-
renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions })),
|
|
758
|
+
renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, officialMenus: menuBridge, forge, fileOpeners: registry, sessions })),
|
|
752
759
|
activeSession: () => {
|
|
753
760
|
const scope = readSessionScope(ctx)
|
|
754
761
|
if (!scope.sessionId) return undefined
|
|
@@ -833,6 +840,14 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
833
840
|
getShortcuts: () => detectShortcuts(ctx),
|
|
834
841
|
})))
|
|
835
842
|
|
|
843
|
+
registerSlotSafely(ctx, { name: 'settings.section.icon', id: 'vscode-mode', order: 30 },
|
|
844
|
+
(input: unknown) => {
|
|
845
|
+
const props = input as { size?: number; active?: boolean }
|
|
846
|
+
return React.createElement(IconCodeOutline16 as React.ComponentType<{ size?: number; className?: string }>, {
|
|
847
|
+
size: props.size ?? 16, className: props.active ? 'edrv-settings-active' : undefined,
|
|
848
|
+
})
|
|
849
|
+
})
|
|
850
|
+
|
|
836
851
|
// 官方插件管理器详情页的只读状态段(DSH 0.2.1 新增扩展位):非本插件页返回 null。
|
|
837
852
|
// 该 slot 在旧版 DSH 不存在,slots.inject 会一直等待(不报错),故无需版本判断。
|
|
838
853
|
registerSlotSafely(ctx, {
|
|
@@ -32,6 +32,8 @@ const docVersions = new Map()
|
|
|
32
32
|
let statusCache = { servers: [], at: 0 }
|
|
33
33
|
let progressListeners = new Set()
|
|
34
34
|
let ready = false
|
|
35
|
+
let statusRequest = null
|
|
36
|
+
let statusEpoch = 0
|
|
35
37
|
|
|
36
38
|
/** 会话 id 变更通知(EditorView 持有权威 sessionId,经窗口事件回填)。 */
|
|
37
39
|
const SESSION_EVENT = 'edrv:lsp-session'
|
|
@@ -54,6 +56,9 @@ export function setLspSession(id) {
|
|
|
54
56
|
syncTimers.clear()
|
|
55
57
|
docVersions.clear()
|
|
56
58
|
sessionId = id
|
|
59
|
+
statusEpoch++
|
|
60
|
+
statusCache = { servers: [], at: 0 }
|
|
61
|
+
ready = false
|
|
57
62
|
void refreshStatus()
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -308,15 +313,28 @@ export async function fetchSemanticTokens(path, text) {
|
|
|
308
313
|
return res.tokens
|
|
309
314
|
}
|
|
310
315
|
|
|
311
|
-
/**
|
|
316
|
+
/** @public @author ddj 2026年11月04号 @param force Bypass cached data, not the in-flight guard. @returns Current-session server status; stale responses never publish. */
|
|
312
317
|
export async function refreshStatus(force = false) {
|
|
318
|
+
if (statusRequest) {
|
|
319
|
+
const pending = statusRequest
|
|
320
|
+
await pending.promise
|
|
321
|
+
if (pending.epoch !== statusEpoch) return refreshStatus(force)
|
|
322
|
+
return statusCache.servers
|
|
323
|
+
}
|
|
324
|
+
if (typeof document !== 'undefined' && document.hidden) return statusCache.servers
|
|
313
325
|
if (!force && Date.now() - statusCache.at < 1500) return statusCache.servers
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
326
|
+
const epoch = statusEpoch, owner = sessionId
|
|
327
|
+
const request = { epoch, promise: rpc('edrv.lsp.status', { sessionId: owner }).catch(() => null) }
|
|
328
|
+
statusRequest = request
|
|
329
|
+
try {
|
|
330
|
+
const res = await request.promise
|
|
331
|
+
if (epoch !== statusEpoch || owner !== sessionId) return statusCache.servers
|
|
332
|
+
const servers = res && res.ok && Array.isArray(res.servers) ? res.servers : []
|
|
333
|
+
statusCache = { servers, at: Date.now() }
|
|
334
|
+
ready = true
|
|
335
|
+
publishProgress()
|
|
336
|
+
return servers
|
|
337
|
+
} finally { if (statusRequest === request) statusRequest = null }
|
|
320
338
|
}
|
|
321
339
|
|
|
322
340
|
export function lspReady() {
|
|
@@ -64,6 +64,7 @@ export interface OfficialTabParams {
|
|
|
64
64
|
preview?: boolean
|
|
65
65
|
/** 目标行号(行引用/工具行跳转透传;缺省仅打开文件)。 */
|
|
66
66
|
line?: number
|
|
67
|
+
column?: number
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
/** sidebarRightTabs 服务的最小结构面(结构性探测,不 import 官方类型)。 */
|
|
@@ -133,10 +134,17 @@ export function resolveNavOpen(params: unknown): { path: string | null; focusDif
|
|
|
133
134
|
export function resolveNavLine(params: unknown): number | undefined {
|
|
134
135
|
const value = (params ?? {}) as OfficialTabParams
|
|
135
136
|
const line = value?.line
|
|
136
|
-
if (typeof line !== 'number' || !Number.isFinite(line) || line
|
|
137
|
+
if (typeof line !== 'number' || !Number.isFinite(line) || line < 1) return undefined
|
|
137
138
|
return Math.floor(line)
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
/** @public @author ddj 2026年11月04号 @param params Plugin navigation extension. @returns Positive column only alongside a valid line; official line-only callers remain unchanged. */
|
|
142
|
+
export function resolveNavColumn(params: unknown): number | undefined {
|
|
143
|
+
if (!resolveNavLine(params)) return undefined
|
|
144
|
+
const column = (params as OfficialTabParams)?.column
|
|
145
|
+
return typeof column === 'number' && Number.isFinite(column) && column >= 1 ? Math.floor(column) : undefined
|
|
146
|
+
}
|
|
147
|
+
|
|
140
148
|
/** 逐段 component 解码(失败返回 null,对齐官方 parseFileAddress 的容错语义)。 */
|
|
141
149
|
function decodeSegment(raw: string | undefined): string | null {
|
|
142
150
|
if (raw === undefined) return null
|
|
@@ -158,7 +166,7 @@ function decodeSegment(raw: string | undefined): string | null {
|
|
|
158
166
|
*/
|
|
159
167
|
export function parseOfficialFileAddress(address: unknown): { path: string; sessionId?: string } | null {
|
|
160
168
|
if (typeof address !== 'string' || !address.startsWith(OFFICIAL_FILE_PREFIX)) return null
|
|
161
|
-
const [scope, ...tail] = address.slice(OFFICIAL_FILE_PREFIX.length).split('/')
|
|
169
|
+
const [scope, ...tail] = address.split(/[?#]/, 1)[0].slice(OFFICIAL_FILE_PREFIX.length).split('/')
|
|
162
170
|
const kind = decodeSegment(scope)
|
|
163
171
|
if (kind === 'session') {
|
|
164
172
|
const sessionId = decodeSegment(tail[0])
|
|
@@ -236,10 +244,14 @@ export function forwardToEditor(
|
|
|
236
244
|
path: string | null,
|
|
237
245
|
line?: number,
|
|
238
246
|
preview?: boolean,
|
|
247
|
+
column?: number,
|
|
239
248
|
): boolean {
|
|
240
249
|
if (!service || typeof service.openTab !== 'function' || !path) return false
|
|
241
250
|
const params: OfficialTabParams = { openPath: path }
|
|
242
|
-
|
|
251
|
+
const validLine = resolveNavLine({ line })
|
|
252
|
+
if (validLine !== undefined) params.line = validLine
|
|
253
|
+
const validColumn = resolveNavColumn({ line: validLine, column })
|
|
254
|
+
if (validColumn !== undefined) params.column = validColumn
|
|
243
255
|
if (preview !== undefined) params.preview = preview
|
|
244
256
|
try {
|
|
245
257
|
// 官方语义:页类型恒去重,故转发只会聚焦既有编辑器页签,不产生新实例
|
package/src/client/reviewBar.ts
CHANGED
|
@@ -126,6 +126,9 @@ export const SIDECAR_KEEP_TITLE = '标记这些历史改动为已看过:只改
|
|
|
126
126
|
/** 非官方 Undo 的作用域说明:走原有回滚校验写回磁盘,但不影响官方记录。 */
|
|
127
127
|
export const SIDECAR_UNDO_TITLE = '撤销这些历史改动(写回受回滚校验保护):只影响这份历史记录,不影响官方审查。'
|
|
128
128
|
|
|
129
|
+
/** 非官方 Keep 之后的完成态:该通道 Keep 不写盘,不能沿用「已采纳」这种会让人以为改过文件的措辞。 */
|
|
130
|
+
export const SIDECAR_KEEP_DONE = '已标记为看过(未改动文件)'
|
|
131
|
+
|
|
129
132
|
/** 当前端没有该会话时,差异栏里给中性说明而不是红色报错。 */
|
|
130
133
|
export const SESSION_DOWN_NOTICE = '这个会话没在当前端激活,官方审查数据暂时读不到;下面的历史改动仍可查看、Keep、Undo。'
|
|
131
134
|
|
|
@@ -30,7 +30,15 @@ export async function loadPending(port: Pick<BatchPort, 'snapshot'>): Promise<Re
|
|
|
30
30
|
return page.items
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* @public
|
|
35
|
+
* @author ddj 2026年10月27号
|
|
36
|
+
* @description: 逐条决策引擎,现仅供 Undo All 使用(Keep 已改走 `reviewKeepAll.ts` 的宿主批量通道);
|
|
37
|
+
* 保留 reject 形态以便对照既有契约与测试。
|
|
38
|
+
* @param port Guarded existing per-file decision adapter.
|
|
39
|
+
* @param reject Undo instead of Keep.
|
|
40
|
+
* @returns Precise partial result; Undo stops at first conflict, never claims atomic multi-file rollback.
|
|
41
|
+
*/
|
|
34
42
|
export async function runReviewBatch(port: BatchPort, reject: boolean): Promise<BatchResult> {
|
|
35
43
|
const result: BatchResult = { completed: 0, total: 0, paths: [], error: '' }
|
|
36
44
|
try {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 批量 Keep:一次完整快照 + 一次批量 RPC,单条基线问题只跳过自己,不再整批中止。
|
|
3
|
+
* Undo 仍走 reviewBatch.ts 的逐条通道(写盘要逐项校验并在冲突处停止)。
|
|
4
|
+
* @author ddj 2026年11月03号
|
|
5
|
+
*/
|
|
6
|
+
import type { KeepAllItem, KeepAllResult, ReviewItem, ReviewPage, ReviewResult } from '../shared/review.js'
|
|
7
|
+
import { loadPending } from './reviewBatch.js'
|
|
8
|
+
|
|
9
|
+
/** 批量 Keep 的 RPC 适配器;guard 在取快照前后各校验一次会话寿命。 */
|
|
10
|
+
export interface KeepAllPort {
|
|
11
|
+
snapshot: () => Promise<ReviewPage>
|
|
12
|
+
keepAll: (items: KeepAllItem[], batchId: string) => Promise<ReviewResult<{ result: KeepAllResult }>>
|
|
13
|
+
guard: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @private
|
|
18
|
+
* @author ddj 2026年11月03号
|
|
19
|
+
* @description: 取一次完整待审快照;完整性校验失败时重取一次,避免瞬时漂移被当成不可恢复错误。
|
|
20
|
+
* @param port 已鉴权的 RPC 适配器
|
|
21
|
+
* @returns 冻结的批量目标
|
|
22
|
+
* @throws 两次都取不到一致快照时抛出原始原因
|
|
23
|
+
*/
|
|
24
|
+
async function snapshotOnce(port: KeepAllPort): Promise<KeepAllItem[]> {
|
|
25
|
+
try {
|
|
26
|
+
return toItems(await loadPending(port))
|
|
27
|
+
} catch {
|
|
28
|
+
// 分页期间数量漂移多为瞬时:guard 仍有效就重新取一次,仍不一致才放弃
|
|
29
|
+
port.guard()
|
|
30
|
+
return toItems(await loadPending(port))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @private
|
|
36
|
+
* @author ddj 2026年11月03号
|
|
37
|
+
* @description: 把快照条目裁成批量目标,只带 host 需要的身份与版本线索。
|
|
38
|
+
* @param items 完整待审集
|
|
39
|
+
* @returns 批量目标
|
|
40
|
+
*/
|
|
41
|
+
function toItems(items: ReviewItem[]): KeepAllItem[] {
|
|
42
|
+
return items.map((item) => ({ id: item.id, display: item.display, seq: item.seq, revision: item.revision }))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @public
|
|
47
|
+
* @author ddj 2026年11月03号
|
|
48
|
+
* @description: 批量保留全部待审记录(Keep 不改写磁盘)。一次往返完成,跳过项只进 skipped,
|
|
49
|
+
* 段级异常才写 error;guard 失效时保留已完成计数交给调用方呈现。
|
|
50
|
+
* @param port 已鉴权的 RPC 适配器
|
|
51
|
+
* @returns 与 BatchResult 同构的批量结果(含跳过明细)
|
|
52
|
+
*/
|
|
53
|
+
export async function runKeepAll(port: KeepAllPort): Promise<KeepAllResult> {
|
|
54
|
+
const result: KeepAllResult = { completed: 0, total: 0, paths: [], skipped: [], error: '' }
|
|
55
|
+
try {
|
|
56
|
+
port.guard()
|
|
57
|
+
const items = await snapshotOnce(port)
|
|
58
|
+
result.total = items.length
|
|
59
|
+
if (!items.length) return result
|
|
60
|
+
|
|
61
|
+
port.guard()
|
|
62
|
+
const reply = await port.keepAll(items, crypto.randomUUID())
|
|
63
|
+
if (!reply.ok) {
|
|
64
|
+
result.error = reply.error
|
|
65
|
+
return result
|
|
66
|
+
}
|
|
67
|
+
return reply.result
|
|
68
|
+
} catch (cause) {
|
|
69
|
+
result.error = cause instanceof Error ? cause.message : String(cause)
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -581,6 +581,12 @@ function svnMenuItems(): TreeMenuItem[] {
|
|
|
581
581
|
export function createDefaultFileMenuItems(): TreeMenuItem[] {
|
|
582
582
|
return [
|
|
583
583
|
...openGroupItems(),
|
|
584
|
+
{ id: 'forge-open', label: '打开 Forge', order: 6, group: GROUP_OPEN,
|
|
585
|
+
visible: (_target, ctx) => ctx.forge?.available() === true,
|
|
586
|
+
run: (_target, ctx) => { if (ctx.sessionId) ctx.forge?.open(ctx.sessionId) } },
|
|
587
|
+
{ id: 'forge-local', label: '查看本地变更(Forge)', order: 7, group: GROUP_OPEN,
|
|
588
|
+
visible: (_target, ctx) => ctx.forge?.available() === true,
|
|
589
|
+
run: (_target, ctx) => { if (ctx.sessionId) ctx.forge?.open(ctx.sessionId, true) } },
|
|
584
590
|
...revealGroupItems(),
|
|
585
591
|
...refGroupItems(),
|
|
586
592
|
...clipGroupItems(),
|
|
@@ -84,11 +84,17 @@ function rowView(props) {
|
|
|
84
84
|
function select() { onSelect(path, isDir) }
|
|
85
85
|
/** @private @author ddj 2026年10月10号 @param event Context trigger. */
|
|
86
86
|
function menu(event) { onContext(path, type, event) }
|
|
87
|
+
/** @private @author ddj 2026年11月04号 @param event Keyboard context trigger anchored to the focused row. */
|
|
88
|
+
function menuKey(event) {
|
|
89
|
+
if (event.key !== 'ContextMenu' && !(event.shiftKey && event.key === 'F10')) return
|
|
90
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
91
|
+
onContext(path, type, { preventDefault: () => event.preventDefault(), stopPropagation: () => event.stopPropagation(), clientX: rect.left, clientY: rect.bottom })
|
|
92
|
+
}
|
|
87
93
|
return React.createElement('div', {
|
|
88
94
|
className: 'edrv-tree-row' + (active ? ' edrv-tree-active' : '') + (context ? ' edrv-tree-context' : '')
|
|
89
95
|
+ (reveal ? ' edrv-tree-reveal' : '') + (mark?.stale ? ' edrv-tree-stale' : ''),
|
|
90
96
|
title: path + (mark?.stale ? ';SVN 状态陈旧,等待确认' : ''), 'data-edrv-path': path,
|
|
91
|
-
style: { paddingLeft: 6 + depth * 14 }, onClick: select, onContextMenu: menu,
|
|
97
|
+
style: { paddingLeft: 6 + depth * 14 }, tabIndex: 0, onClick: select, onContextMenu: menu, onKeyDown: menuKey,
|
|
92
98
|
},
|
|
93
99
|
React.createElement('span', { className: 'edrv-tree-chev' }, isDir ? (isOpen ? '▾' : '▸') : ''),
|
|
94
100
|
React.createElement('span', { className: 'edrv-tree-iconbox' }, rowIconEl({ name }, isDir, isOpen), svnOverlayEl(viewOverlayOf(mark), remote)),
|
|
@@ -480,6 +486,25 @@ export function FileExplorer(props) {
|
|
|
480
486
|
/** @private @author ddj 2026年10月22号 Close stale path menus instead of rebinding recycled rows. */
|
|
481
487
|
function closeLostMenu() { if (menu && !menuValid) setMenu(null) }
|
|
482
488
|
React.useEffect(closeLostMenu, [menu, menuValid])
|
|
489
|
+
const liveMenu = React.useRef({ scope, treeRows, sessionId: ctx?.sessionId, alive: true })
|
|
490
|
+
const openingValid = React.useRef(null)
|
|
491
|
+
liveMenu.current = { scope, treeRows, sessionId: ctx?.sessionId, alive: liveMenu.current.alive }
|
|
492
|
+
React.useEffect(() => {
|
|
493
|
+
liveMenu.current.alive = ctx?.visible !== false
|
|
494
|
+
return () => { liveMenu.current.alive = false; if (openingValid.current) ctx?.officialMenus?.invalidate(openingValid.current) }
|
|
495
|
+
}, [scope, ctx?.sessionId, ctx?.officialMenus, ctx?.visible])
|
|
496
|
+
React.useEffect(() => {
|
|
497
|
+
if (!menuValid || !ctx?.officialMenus || !ctx.sessionId) return
|
|
498
|
+
const target = { ...menu.target }
|
|
499
|
+
const sessionId = ctx.sessionId
|
|
500
|
+
const valid = () => liveMenu.current.alive && liveMenu.current.sessionId === sessionId && liveMenu.current.scope === menu.scope && (target.path === '' || liveMenu.current.treeRows.some((row) => row.e?.path === target.path))
|
|
501
|
+
openingValid.current = valid
|
|
502
|
+
const items = buildTreeMenu(ctx.fileMenuItems, target, ctx).map((item) => ({
|
|
503
|
+
id: item.id, label: item.label, disabled: item.disabled, danger: item.danger, separator: item.separator,
|
|
504
|
+
run: () => item.run(target, ctx),
|
|
505
|
+
}))
|
|
506
|
+
if (ctx.officialMenus.open('resource', ctx.sessionId, target.path, { x: menu.x, y: menu.y }, items, valid)) setMenu(null)
|
|
507
|
+
}, [menu, menuValid, ctx?.officialMenus])
|
|
483
508
|
// 当前右键目标的可视菜单项(构建时过滤/排序,registry 变化下次打开生效)。
|
|
484
509
|
const menuEntries = menuValid
|
|
485
510
|
? buildTreeMenu(ctx?.fileMenuItems, menu.target, ctx).map((item) => ({
|
|
@@ -11,6 +11,7 @@ import type { TreeMenuRegistry } from './contextMenu.js'
|
|
|
11
11
|
/** 面板可用的共享上下文(由 SidebarView 从 EditorView 注入,面板不直碰其内部)。 */
|
|
12
12
|
export interface SidebarCtx {
|
|
13
13
|
sessionId?: string
|
|
14
|
+
visible?: boolean
|
|
14
15
|
/** 当前会话工作区 cwd(来自 sessions 快照;规则面板项目 Tab 自动匹配用)。 */
|
|
15
16
|
cwd?: string | null
|
|
16
17
|
/** 状态作用域键(工作区优先,无 cwd 回退会话;面板持久化统一走它,scopeStore 产出)。 */
|
|
@@ -31,6 +32,8 @@ export interface SidebarCtx {
|
|
|
31
32
|
outlineSources?: OutlineSourceRegistry
|
|
32
33
|
/** 文件右键菜单项注册表(公开 provide 为 edrvFileContextMenuItems;文件管理面板构建菜单用)。 */
|
|
33
34
|
fileMenuItems?: TreeMenuRegistry
|
|
35
|
+
officialMenus?: import('../contextMenusOfficial.js').OfficialMenuBridge
|
|
36
|
+
forge?: import('../forgeBridge.js').ForgeBridge
|
|
34
37
|
/** 「添加到对话」动作集(文件/文件夹引用注入对话输入框;缺省时菜单项降级提示)。 */
|
|
35
38
|
addToConversation?: AddToConversation
|
|
36
39
|
/** SVN 能力状态(未加载/未检出为 null;SVN 菜单项显隐判定)。 */
|
|
@@ -32,6 +32,39 @@ export function officialPath(path: string, page: ReviewPage | null): boolean {
|
|
|
32
32
|
return !!page?.overview?.coveredPaths.some((candidate) => diffPathKey(candidate) === key)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @author ddj 2026年11月3号
|
|
37
|
+
* @description: 本页待审条目占用的路径键:路径本身与其身份各一份。
|
|
38
|
+
* @param page 审查页
|
|
39
|
+
* @returns 待审覆盖的路径键集合
|
|
40
|
+
*/
|
|
41
|
+
function pendingCoverKeys(page: ReviewPage | null): Set<string> {
|
|
42
|
+
const keys = new Set<string>()
|
|
43
|
+
for (const item of page?.items ?? []) {
|
|
44
|
+
if (!item.id.startsWith(PENDING_PREFIX)) continue
|
|
45
|
+
// 待审条目的 path 取自写入时的原始写法(agent 常用工作区相对路径),官方导航用的却是绝对身份:
|
|
46
|
+
// 只按 path 建覆盖集时,同一文件的官方汇总行不会被压掉,差异栏把 3 个文件算成 6 条。
|
|
47
|
+
// 身份(id 去掉待审前缀)与官方 identity 同源,必须一起入集。
|
|
48
|
+
keys.add(diffPathKey(item.currentPath || item.path))
|
|
49
|
+
keys.add(diffPathKey(item.id.slice(PENDING_PREFIX.length)))
|
|
50
|
+
}
|
|
51
|
+
return keys
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @public
|
|
56
|
+
* @author ddj 2026年11月3号
|
|
57
|
+
* @description: 取本页"已有权威通道在管"的路径键:官方汇总覆盖集 + 待审条目。
|
|
58
|
+
* 编辑器与差异汇总必须同口径,否则同一改动在列表里被压掉、在编辑器里却仍然画得出来并可以点。
|
|
59
|
+
* @param page 审查页
|
|
60
|
+
* @returns 路径比较键集合
|
|
61
|
+
*/
|
|
62
|
+
export function coveredPathKeys(page: ReviewPage | null): Set<string> {
|
|
63
|
+
const keys = pendingCoverKeys(page)
|
|
64
|
+
for (const path of page?.overview?.coveredPaths ?? []) keys.add(diffPathKey(path))
|
|
65
|
+
return keys
|
|
66
|
+
}
|
|
67
|
+
|
|
35
68
|
/**
|
|
36
69
|
* @author ddj 2026年10月30号
|
|
37
70
|
* @description: 取出本页待审条目(id 带待审前缀)并折算成文件行。
|
|
@@ -40,15 +73,9 @@ export function officialPath(path: string, page: ReviewPage | null): boolean {
|
|
|
40
73
|
*/
|
|
41
74
|
function pendingRowsOf(page: ReviewPage | null): { rows: DiffFile[]; covered: Set<string> } {
|
|
42
75
|
const rows: DiffFile[] = []
|
|
43
|
-
const covered = new Set<string>()
|
|
44
76
|
for (const item of page?.items ?? []) {
|
|
45
77
|
if (!item.id.startsWith(PENDING_PREFIX)) continue
|
|
46
78
|
const path = item.currentPath || item.path
|
|
47
|
-
// 待审条目的 path 取自写入时的原始写法(agent 常用工作区相对路径),官方导航用的却是绝对身份:
|
|
48
|
-
// 只按 path 建覆盖集时,同一文件的官方汇总行不会被压掉,差异栏把 3 个文件算成 6 条。
|
|
49
|
-
// 身份(id 去掉待审前缀)与官方 identity 同源,必须一起入集。
|
|
50
|
-
covered.add(diffPathKey(path))
|
|
51
|
-
covered.add(diffPathKey(item.id.slice(PENDING_PREFIX.length)))
|
|
52
79
|
rows.push({
|
|
53
80
|
path,
|
|
54
81
|
label: item.display || path,
|
|
@@ -59,7 +86,7 @@ function pendingRowsOf(page: ReviewPage | null): { rows: DiffFile[]; covered: Se
|
|
|
59
86
|
sourceKind: 'pending',
|
|
60
87
|
})
|
|
61
88
|
}
|
|
62
|
-
return { rows, covered }
|
|
89
|
+
return { rows, covered: pendingCoverKeys(page) }
|
|
63
90
|
}
|
|
64
91
|
|
|
65
92
|
/** @public @author ddj 2026年10月26号 @param records Tool-call history. @param page Official file navigation, never only its first 50 rows. @returns Pending first, then official; legacy paths remain explicitly labelled. */
|
|
@@ -30,6 +30,11 @@ interface FeedEntry {
|
|
|
30
30
|
onRefresh: (() => void) | null
|
|
31
31
|
seq: number
|
|
32
32
|
inflight: boolean
|
|
33
|
+
queued?: boolean
|
|
34
|
+
queuedSkipStale?: boolean
|
|
35
|
+
request?: Promise<void>
|
|
36
|
+
controller?: AbortController
|
|
37
|
+
onVisible?: () => void
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
const feeds = new Map<string, FeedEntry>()
|
|
@@ -80,12 +85,21 @@ function notifyRecords(entry: FeedEntry): void {
|
|
|
80
85
|
*/
|
|
81
86
|
export function refreshFeed(sessionId: string, skipStale = false): Promise<void> {
|
|
82
87
|
const entry = entryOf(sessionId)
|
|
88
|
+
entry.queuedSkipStale = entry.queuedSkipStale === undefined ? skipStale : entry.queuedSkipStale && skipStale
|
|
89
|
+
if (typeof document !== 'undefined' && document.hidden) { entry.queued = true; return Promise.resolve() }
|
|
90
|
+
if (entry.inflight) {
|
|
91
|
+
entry.queued = true
|
|
92
|
+
return entry.request ?? Promise.resolve()
|
|
93
|
+
}
|
|
94
|
+
skipStale = entry.queuedSkipStale ?? skipStale
|
|
95
|
+
entry.queuedSkipStale = undefined
|
|
83
96
|
const current = ++entry.seq
|
|
84
97
|
entry.inflight = true
|
|
98
|
+
entry.controller = new AbortController()
|
|
85
99
|
const payload: { sessionId: string; skipStale?: boolean } = { sessionId }
|
|
86
100
|
if (skipStale) payload.skipStale = true
|
|
87
101
|
|
|
88
|
-
|
|
102
|
+
entry.request = rpc('edrv.list', payload, entry.controller.signal)
|
|
89
103
|
.then((res) => {
|
|
90
104
|
if (current !== entry.seq) return
|
|
91
105
|
if (!res || !res.ok || !Array.isArray(res.records)) return
|
|
@@ -105,8 +119,14 @@ export function refreshFeed(sessionId: string, skipStale = false): Promise<void>
|
|
|
105
119
|
}
|
|
106
120
|
})
|
|
107
121
|
.finally(() => {
|
|
108
|
-
if (current
|
|
122
|
+
if (current !== entry.seq) return
|
|
123
|
+
entry.inflight = false
|
|
124
|
+
if (entry.queued) {
|
|
125
|
+
entry.queued = false
|
|
126
|
+
if (feeds.get(sessionId) === entry) return refreshFeed(sessionId, entry.queuedSkipStale ?? false)
|
|
127
|
+
}
|
|
109
128
|
})
|
|
129
|
+
return entry.request
|
|
110
130
|
}
|
|
111
131
|
|
|
112
132
|
/**
|
|
@@ -118,6 +138,7 @@ export function refreshFeed(sessionId: string, skipStale = false): Promise<void>
|
|
|
118
138
|
function ensureTimer(sessionId: string, entry: FeedEntry): void {
|
|
119
139
|
if (entry.timer === null) {
|
|
120
140
|
entry.timer = setInterval(() => {
|
|
141
|
+
if (typeof document !== 'undefined' && document.hidden) return
|
|
121
142
|
const tickers = Array.from(entry.tickers)
|
|
122
143
|
for (const tick of tickers) {
|
|
123
144
|
try {
|
|
@@ -130,6 +151,14 @@ function ensureTimer(sessionId: string, entry: FeedEntry): void {
|
|
|
130
151
|
if (entry.listeners.size > 0 || entry.errorListeners.size > 0) void refreshFeed(sessionId)
|
|
131
152
|
}, FEED_INTERVAL_MS)
|
|
132
153
|
}
|
|
154
|
+
if (!entry.onVisible && typeof document !== 'undefined') {
|
|
155
|
+
entry.onVisible = () => {
|
|
156
|
+
if (document.hidden) return
|
|
157
|
+
for (const tick of entry.tickers) { try { tick() } catch { /* Isolated observer. */ } }
|
|
158
|
+
if (entry.listeners.size || entry.errorListeners.size) { entry.queued = false; void refreshFeed(sessionId) }
|
|
159
|
+
}
|
|
160
|
+
document.addEventListener('visibilitychange', entry.onVisible)
|
|
161
|
+
}
|
|
133
162
|
if (entry.onRefresh === null) {
|
|
134
163
|
const handler = () => {
|
|
135
164
|
void refreshFeed(sessionId)
|
|
@@ -147,12 +176,16 @@ function ensureTimer(sessionId: string, entry: FeedEntry): void {
|
|
|
147
176
|
* @param entry 订阅槽位
|
|
148
177
|
*/
|
|
149
178
|
function releaseIfIdle(sessionId: string, entry: FeedEntry): void {
|
|
150
|
-
if (entry.listeners.size || entry.tickers.size || entry.errorListeners.size) return
|
|
179
|
+
if (feeds.get(sessionId) !== entry || entry.listeners.size || entry.tickers.size || entry.errorListeners.size) return
|
|
151
180
|
if (entry.timer !== null) clearInterval(entry.timer)
|
|
152
181
|
if (entry.onRefresh !== null) {
|
|
153
182
|
const target = win()
|
|
154
183
|
if (target) target.removeEventListener('edrv:refresh', entry.onRefresh)
|
|
155
184
|
}
|
|
185
|
+
entry.seq++
|
|
186
|
+
entry.queued = false
|
|
187
|
+
entry.controller?.abort()
|
|
188
|
+
if (entry.onVisible && typeof document !== 'undefined') document.removeEventListener('visibilitychange', entry.onVisible)
|
|
156
189
|
feeds.delete(sessionId)
|
|
157
190
|
}
|
|
158
191
|
|
|
@@ -237,5 +270,9 @@ export function disposeFeed(sessionId: string): void {
|
|
|
237
270
|
const target = win()
|
|
238
271
|
if (target) target.removeEventListener('edrv:refresh', entry.onRefresh)
|
|
239
272
|
}
|
|
273
|
+
entry.seq++
|
|
274
|
+
entry.queued = false
|
|
275
|
+
entry.controller?.abort()
|
|
276
|
+
if (entry.onVisible && typeof document !== 'undefined') document.removeEventListener('visibilitychange', entry.onVisible)
|
|
240
277
|
feeds.delete(sessionId)
|
|
241
278
|
}
|