dsh-vscode-mode 0.16.0 → 0.17.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 +13 -9
- package/lib/client.js +376 -84
- package/lib/client.js.map +1 -1
- package/lib/index.js +4391 -4350
- package/lib/index.js.map +1 -1
- package/locale/en.json +4 -2
- package/locale/zh.json +4 -2
- package/package.json +4 -2
- package/src/capture.ts +2 -2
- package/src/client/compat.ts +2 -1
- package/src/client/index.ts +22 -7
- package/src/client/localeClient.ts +97 -0
- package/src/client/officialSidebar.ts +4 -1
- package/src/client/state/recordsFeed.ts +241 -0
- package/src/client/ui/ConversationDiffDock.ts +10 -19
- package/src/client/ui/DiffBadge.ts +12 -19
- package/src/client/ui/EditorView.ts +38 -33
- package/src/client/ui/PerfSizeBadge.ts +7 -4
- package/src/client/ui/PluginDetailSection.ts +82 -0
- package/src/dshVersion.ts +4 -2
- package/src/fileVersions.ts +2 -2
- package/src/lsp/rpc.ts +2 -2
- package/src/mcpRuntime.ts +2 -1
- package/src/registry.ts +15 -2
- package/src/rpc.ts +2 -2
- package/src/rules.ts +9 -4
- package/src/search/fallback.ts +2 -2
- package/src/search/ripgrep.ts +2 -2
- package/src/store.ts +26 -1
- package/src/svn.ts +2 -2
package/locale/en.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "VSCodeMode",
|
|
4
|
+
"description": "VSCode-like coding on DSH: Monaco editor with tabs and QuickOpen, command palette, agent edit review, SVN integration, and LSP smartness."
|
|
5
|
+
}
|
|
4
6
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "VSCodeMode",
|
|
4
|
+
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(页签/QuickOpen/状态栏)、命令栏、Agent 编辑差异审查、SVN 集成与 LSP 智能。"
|
|
5
|
+
}
|
|
4
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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": [
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"type": "module",
|
|
31
31
|
"engines": {
|
|
32
|
-
"node": "^22.19.0 || >=24.0.0"
|
|
32
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
33
|
+
"dsh": ">=0.1.0-rc.1 <0.3.0-0"
|
|
33
34
|
},
|
|
34
35
|
"packageManager": "pnpm@11.3.0",
|
|
35
36
|
"main": "./lib/index.js",
|
|
@@ -90,6 +91,7 @@
|
|
|
90
91
|
"exports": {
|
|
91
92
|
".": "./lib/index.js",
|
|
92
93
|
"./client": "./lib/client.js",
|
|
94
|
+
"./locale/*.json": "./locale/*.json",
|
|
93
95
|
"./package.json": "./package.json"
|
|
94
96
|
},
|
|
95
97
|
"dsh": {
|
package/src/capture.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { Ctx, Session } from './store.js'
|
|
|
16
16
|
import { READ_CAP, archiveCanceled, loadBucket, saveBucket } from './store.js'
|
|
17
17
|
import { fileMaxBatch, prune } from './model.js'
|
|
18
18
|
import type { Registry } from './registry.js'
|
|
19
|
-
import {
|
|
19
|
+
import { cwdOfSession } from './registry.js'
|
|
20
20
|
import { findRemoteWs, mirrorTargetOf } from './remoteWorkspace.js'
|
|
21
21
|
import { invalidateIndex } from './treeIndex.js'
|
|
22
22
|
import { log } from './log.js'
|
|
@@ -275,7 +275,7 @@ export async function captureToolResult(ctx: Ctx, registry: Registry, exec: any,
|
|
|
275
275
|
// remote_ssh_write 只依赖参数与 isError(其 result.value 形状不受本插件约束)
|
|
276
276
|
if (exec.name !== 'remote_ssh_write' && !result?.value) return
|
|
277
277
|
try {
|
|
278
|
-
const cwd =
|
|
278
|
+
const cwd = cwdOfSession(ctx, session)
|
|
279
279
|
if (!cwd) return
|
|
280
280
|
const args = ((exec.arguments || {}) as Record<string, unknown>)
|
|
281
281
|
const record = exec.name === 'remote_ssh_write'
|
package/src/client/compat.ts
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { DiffBadge } from './ui/DiffBadge.js'
|
|
|
20
20
|
import { PerfSizeBadge } from './ui/PerfSizeBadge.js'
|
|
21
21
|
import { ConversationDiffDock } from './ui/ConversationDiffDock.js'
|
|
22
22
|
import { McpSettings } from './ui/McpSettings.js'
|
|
23
|
+
import { PluginDetailSection } from './ui/PluginDetailSection.js'
|
|
23
24
|
import { rpc } from './rpc.js'
|
|
24
25
|
import { loadMonaco } from './monaco/loader.js'
|
|
25
26
|
import { observeScheme, schemeOfSnapshot } from './monaco/theme.js'
|
|
@@ -60,6 +61,7 @@ import { SHORTCUT_PROFILES, defaultKeybindings, normalizeKeybindings } from '../
|
|
|
60
61
|
import { sidebarMinApply } from './sidebarMin.js'
|
|
61
62
|
import { editorLimitApply } from './editorLimit.js'
|
|
62
63
|
import { log } from './log.js'
|
|
64
|
+
import { installLocale } from './localeClient.js'
|
|
63
65
|
import { setupLsp, setSession, disposeLsp } from './monaco/lsp/index.js'
|
|
64
66
|
import { disposeSnippets } from './snippets/provider.js'
|
|
65
67
|
import { disposeLaunchJson } from './dap/launchSnippetProvider.js'
|
|
@@ -123,6 +125,10 @@ function setupCommands(ctx: any): CommandBridge {
|
|
|
123
125
|
export async function apply(ctx: any): Promise<void> {
|
|
124
126
|
const schedule = (fn: () => void, ms: number) => ctx.timeout(fn, ms)
|
|
125
127
|
|
|
128
|
+
// UI 壳层文案:注册本插件词典到官方 locale 服务(旧版无该服务时直读中文词典)。
|
|
129
|
+
// slot label / 编辑 Tab 标题一律写成 thunk,让语言切换后无需重注册即可刷新。
|
|
130
|
+
const t = installLocale(ctx)
|
|
131
|
+
|
|
126
132
|
/**
|
|
127
133
|
* 无门控上报界面状态镜像事件(绕过客户端 edrv.debug 开关,直接落 host 诊断日志)。
|
|
128
134
|
* 开关关闭时 dbg 直接 return,「重启后没恢复」这类问题会在客户端侧彻底无痕,故另开一条通路。
|
|
@@ -506,7 +512,7 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
506
512
|
name: 'conversation.view',
|
|
507
513
|
id: 'edrv-editor',
|
|
508
514
|
order: 5,
|
|
509
|
-
label: '
|
|
515
|
+
label: () => t('slot.fileEditor'),
|
|
510
516
|
inject: (sessionId: string) => ({ sessionId }),
|
|
511
517
|
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions })))
|
|
512
518
|
}
|
|
@@ -530,9 +536,10 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
530
536
|
service: official.service,
|
|
531
537
|
slots: ctx.slots,
|
|
532
538
|
renderTab: officialRenderTab,
|
|
539
|
+
title: () => t('slot.fileEditor'),
|
|
533
540
|
guide: {
|
|
534
|
-
title: () =>
|
|
535
|
-
description: () => '
|
|
541
|
+
title: () => t('guide.title'),
|
|
542
|
+
description: () => t('guide.description'),
|
|
536
543
|
},
|
|
537
544
|
})
|
|
538
545
|
if (outcome === null) {
|
|
@@ -632,16 +639,16 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
632
639
|
name: 'conversation.input.dock',
|
|
633
640
|
id: 'edrv-diff-dock',
|
|
634
641
|
order: 30,
|
|
635
|
-
label: '
|
|
642
|
+
label: () => t('slot.diff'),
|
|
636
643
|
inject: (sessionId: string) => ({ sessionId }),
|
|
637
644
|
}, (props: unknown) => React.createElement(ConversationDiffDock, Object.assign({}, props)))
|
|
638
645
|
|
|
639
646
|
// header 差异角标:仅当前工作区存在差异时渲染
|
|
640
|
-
registerSlotSafely(ctx, { name: 'conversation.session.header.utilities', id: 'edrv-diff-badge', order: 90, label: '
|
|
647
|
+
registerSlotSafely(ctx, { name: 'conversation.session.header.utilities', id: 'edrv-diff-badge', order: 90, label: () => t('slot.diff') },
|
|
641
648
|
(props: unknown) => React.createElement(DiffBadge, Object.assign({}, props)))
|
|
642
649
|
|
|
643
650
|
// header 会话体积指示器:当前会话持久化体积,超阈值提示新开会话(性能优化)
|
|
644
|
-
registerSlotSafely(ctx, { name: 'conversation.session.header.utilities', id: 'edrv-perf-size', order: 89, label: '
|
|
651
|
+
registerSlotSafely(ctx, { name: 'conversation.session.header.utilities', id: 'edrv-perf-size', order: 89, label: () => t('slot.sessionSize') },
|
|
645
652
|
(props: unknown) => React.createElement(PerfSizeBadge, Object.assign({}, props, { sessions })))
|
|
646
653
|
|
|
647
654
|
// 设置页中的 VSCodeMode 专属页签,MCP 管理作为该页签的内部子 Tab。
|
|
@@ -649,7 +656,7 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
649
656
|
name: 'settings.section',
|
|
650
657
|
id: 'vscode-mode',
|
|
651
658
|
order: 30,
|
|
652
|
-
label: '
|
|
659
|
+
label: () => t('slot.settings'),
|
|
653
660
|
}, () => React.createElement(SettingsContext.Provider, { value: bridge.scope() }, React.createElement(McpSettings, {
|
|
654
661
|
openerRegistry: registry,
|
|
655
662
|
compatSummary,
|
|
@@ -658,6 +665,14 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
658
665
|
getShortcuts: () => detectShortcuts(ctx),
|
|
659
666
|
})))
|
|
660
667
|
|
|
668
|
+
// 官方插件管理器详情页的只读状态段(DSH 0.2.1 新增扩展位):非本插件页返回 null。
|
|
669
|
+
// 该 slot 在旧版 DSH 不存在,slots.inject 会一直等待(不报错),故无需版本判断。
|
|
670
|
+
registerSlotSafely(ctx, {
|
|
671
|
+
name: 'plugins.detail.section',
|
|
672
|
+
id: 'edrv-plugin-status',
|
|
673
|
+
order: 40,
|
|
674
|
+
}, (props: unknown) => React.createElement(PluginDetailSection, Object.assign({}, props)))
|
|
675
|
+
|
|
661
676
|
// 卸载收尾(G4,DSH 0.1.6-alpha.2 起支持运行时卸载/重载):
|
|
662
677
|
// 注销挂在存活的 window.monaco 上的全部 Monaco provider 并复位模块状态。
|
|
663
678
|
// 不做会导致重载后重复注册(补全/跳转/hover 各翻倍)与陈旧开关残留。
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — UI 壳层文案的 locale 接入。
|
|
3
|
+
* 此前 `locale` 写在 inject 里却零调用,界面文案全为硬编码中文;
|
|
4
|
+
* 这里只覆盖壳层(slot label、编辑 Tab 标题、侧栏引导),编辑器内部大段文案不动。
|
|
5
|
+
* 所有对外函数都对 locale 服务做可选探测:旧版 DSH 无该服务时回落中文词典。
|
|
6
|
+
* 作者 ddj 2026年10月19号
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** locale 命名空间(与包名一致,避免与其他插件串词条)。 */
|
|
10
|
+
export const LOCALE_NS = 'dsh-vscode-mode'
|
|
11
|
+
|
|
12
|
+
/** 支持的语言;zh 为缺省回退语言。 */
|
|
13
|
+
export const LOCALE_FALLBACK = 'zh'
|
|
14
|
+
|
|
15
|
+
/** 壳层文案词典(键名即 slot label / Tab 标题的稳定标识)。 */
|
|
16
|
+
export const LOCALE_DICTS: Record<string, Record<string, string>> = {
|
|
17
|
+
zh: {
|
|
18
|
+
'slot.fileEditor': '文件编辑',
|
|
19
|
+
'slot.diff': '差异',
|
|
20
|
+
'slot.sessionSize': '会话体积',
|
|
21
|
+
'slot.settings': 'VSCodeMode',
|
|
22
|
+
'guide.title': '文件编辑',
|
|
23
|
+
'guide.description': 'Monaco 文件编辑器:对话与编辑同屏(差异审查/跳转/大纲)',
|
|
24
|
+
},
|
|
25
|
+
en: {
|
|
26
|
+
'slot.fileEditor': 'File editor',
|
|
27
|
+
'slot.diff': 'Diff',
|
|
28
|
+
'slot.sessionSize': 'Session size',
|
|
29
|
+
'slot.settings': 'VSCodeMode',
|
|
30
|
+
'guide.title': 'File editor',
|
|
31
|
+
'guide.description': 'Monaco file editor: chat and editing side by side (diff review, jump, outline)',
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** locale 服务的最小结构面(结构性探测,不 import 官方类型)。 */
|
|
36
|
+
export interface LocaleLike {
|
|
37
|
+
register?: (ns: string, dicts: Record<string, Record<string, string>>) => unknown
|
|
38
|
+
bind?: (ns: string) => (key: string) => string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 翻译函数:键 → 当前语言文案(找不到时回落中文,再回落键本身)。 */
|
|
42
|
+
export type Translate = (key: string) => string
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 取词条:先当前语言、再中文、最后键本身。
|
|
46
|
+
* @author ddj 2026年10月19号
|
|
47
|
+
* @param lang 语言 id
|
|
48
|
+
* @param key 词条键
|
|
49
|
+
* @returns 文案
|
|
50
|
+
*/
|
|
51
|
+
function lookup(lang: string, key: string): string {
|
|
52
|
+
return LOCALE_DICTS[lang]?.[key] ?? LOCALE_DICTS[LOCALE_FALLBACK]?.[key] ?? key
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 生成不依赖 locale 服务的直读翻译函数(服务缺失/注册失败时使用)。
|
|
57
|
+
* @author ddj 2026年10月19号
|
|
58
|
+
* @param lang 语言 id
|
|
59
|
+
* @returns 翻译函数
|
|
60
|
+
*/
|
|
61
|
+
export function directTranslate(lang: string = LOCALE_FALLBACK): Translate {
|
|
62
|
+
return (key: string) => lookup(lang, key)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 注册本插件词典并返回翻译函数。
|
|
67
|
+
* 服务缺失、重复注册或 bind 抛错都不影响插件其余部分:一律回落直读词典。
|
|
68
|
+
* @author ddj 2026年10月19号
|
|
69
|
+
* @param ctx DSH 客户端上下文
|
|
70
|
+
* @returns 翻译函数(读取时才解析当前语言,可直接放进 slot label thunk)
|
|
71
|
+
*/
|
|
72
|
+
export function installLocale(ctx: { get: (name: string) => unknown }): Translate {
|
|
73
|
+
let service: LocaleLike | undefined
|
|
74
|
+
try {
|
|
75
|
+
service = ctx.get('locale') as LocaleLike | undefined
|
|
76
|
+
} catch {
|
|
77
|
+
// 服务缺失或探测抛错:后续一律按未命中处理。
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 注册失败(例如同名 ns 已被占)不应连带废掉 bind,故两段各自 try。
|
|
81
|
+
try {
|
|
82
|
+
if (service && typeof service.register === 'function') service.register(LOCALE_NS, LOCALE_DICTS)
|
|
83
|
+
} catch {
|
|
84
|
+
// 词典未注册成功:仍尝试 bind,最后还有直读词典兜底。
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
if (service && typeof service.bind === 'function') {
|
|
89
|
+
const bound = service.bind(LOCALE_NS)
|
|
90
|
+
if (typeof bound === 'function') return (key: string) => bound(key)
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// bind 异常按未命中处理。
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return directTranslate()
|
|
97
|
+
}
|
|
@@ -286,8 +286,11 @@ export function installOfficial(options: {
|
|
|
286
286
|
slots: SlotsLike
|
|
287
287
|
renderTab: unknown
|
|
288
288
|
guide: { title: () => string; description: () => string }
|
|
289
|
+
/** Tab 标题 thunk(缺省用内置中文常量;传入后由调用方按当前语言解析)。 */
|
|
290
|
+
title?: () => string
|
|
289
291
|
}): (() => void) | null {
|
|
290
292
|
const { tabs, service, slots, renderTab, guide } = options
|
|
293
|
+
const titleOf = typeof options.title === 'function' ? options.title : () => OFFICIAL_TAB_TITLE
|
|
291
294
|
const open = (path: string | null, focusDiff: boolean): boolean => {
|
|
292
295
|
try {
|
|
293
296
|
// 官方语义:打开即展开侧栏;seat 未挂载(无会话画面)时抛错,降级其他打开器
|
|
@@ -305,7 +308,7 @@ export function installOfficial(options: {
|
|
|
305
308
|
id: OFFICIAL_TAB_ID,
|
|
306
309
|
kind: OFFICIAL_TAB_KIND,
|
|
307
310
|
priority: 'extension',
|
|
308
|
-
title: () =>
|
|
311
|
+
title: () => titleOf(),
|
|
309
312
|
guide: [{ order: 20, title: guide.title, description: guide.description }],
|
|
310
313
|
})
|
|
311
314
|
} catch (error) {
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 会话级差异记录共享订阅。
|
|
3
|
+
* 原先 DiffBadge / ConversationDiffDock / EditorView 各自 5s 轮询同一个
|
|
4
|
+
* `edrv.list`,同一会话一轮要打 3 次 RPC。本模块按会话收敛为单一定时器:
|
|
5
|
+
* 订阅者共享一次拉取,`edrv:refresh` 事件与主动刷新也走同一条通道。
|
|
6
|
+
* 作者 ddj 2026年10月19号
|
|
7
|
+
*/
|
|
8
|
+
import type { RecordView } from '../../shared/types.js'
|
|
9
|
+
import { rpc } from '../rpc.js'
|
|
10
|
+
|
|
11
|
+
/** 兜底轮询周期(毫秒):仅作事件丢失时的兜底,正常由 edrv:refresh 驱动。 */
|
|
12
|
+
export const FEED_INTERVAL_MS = 5000
|
|
13
|
+
|
|
14
|
+
/** 记录变化回调。 */
|
|
15
|
+
export type RecordsListener = (records: RecordView[]) => void
|
|
16
|
+
|
|
17
|
+
/** 周期心跳回调(给只关心「该刷新了」的订阅者复用同一定时器)。 */
|
|
18
|
+
export type TickListener = () => void
|
|
19
|
+
|
|
20
|
+
/** 拉取失败回调。 */
|
|
21
|
+
export type ErrorListener = (message: string) => void
|
|
22
|
+
|
|
23
|
+
/** 单会话的共享订阅状态。 */
|
|
24
|
+
interface FeedEntry {
|
|
25
|
+
records: RecordView[] | null
|
|
26
|
+
listeners: Set<RecordsListener>
|
|
27
|
+
tickers: Set<TickListener>
|
|
28
|
+
errorListeners: Set<ErrorListener>
|
|
29
|
+
timer: ReturnType<typeof setInterval> | null
|
|
30
|
+
onRefresh: (() => void) | null
|
|
31
|
+
seq: number
|
|
32
|
+
inflight: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const feeds = new Map<string, FeedEntry>()
|
|
36
|
+
|
|
37
|
+
/** 取全局 window(Node 测试环境无 window)。 */
|
|
38
|
+
function win(): (Window & typeof globalThis) | undefined {
|
|
39
|
+
return typeof window === 'undefined' ? undefined : window
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 取或建会话订阅槽位。
|
|
44
|
+
* @author ddj 2026年10月19号
|
|
45
|
+
* @param sessionId 会话 id
|
|
46
|
+
* @returns 订阅槽位
|
|
47
|
+
*/
|
|
48
|
+
function entryOf(sessionId: string): FeedEntry {
|
|
49
|
+
let entry = feeds.get(sessionId)
|
|
50
|
+
if (!entry) {
|
|
51
|
+
entry = { records: null, listeners: new Set(), tickers: new Set(), errorListeners: new Set(), timer: null, onRefresh: null, seq: 0, inflight: false }
|
|
52
|
+
feeds.set(sessionId, entry)
|
|
53
|
+
}
|
|
54
|
+
return entry
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 把最新记录分发给订阅者(快照相等时也分发,交由消费方自行比对)。
|
|
59
|
+
* @author ddj 2026年10月19号
|
|
60
|
+
* @param entry 订阅槽位
|
|
61
|
+
*/
|
|
62
|
+
function notifyRecords(entry: FeedEntry): void {
|
|
63
|
+
const snapshot = entry.records
|
|
64
|
+
if (!snapshot) return
|
|
65
|
+
for (const listener of Array.from(entry.listeners)) {
|
|
66
|
+
try {
|
|
67
|
+
listener(snapshot)
|
|
68
|
+
} catch {
|
|
69
|
+
// 单个订阅者异常不得影响其余订阅者。
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 拉取一次该会话的差异记录。
|
|
76
|
+
* @author ddj 2026年10月19号
|
|
77
|
+
* @param sessionId 会话 id
|
|
78
|
+
* @param skipStale 是否跳过过期判定(与 edrv.list 参数同名同义)
|
|
79
|
+
* @returns 拉取完成 Promise
|
|
80
|
+
*/
|
|
81
|
+
export function refreshFeed(sessionId: string, skipStale = false): Promise<void> {
|
|
82
|
+
const entry = entryOf(sessionId)
|
|
83
|
+
const current = ++entry.seq
|
|
84
|
+
entry.inflight = true
|
|
85
|
+
const payload: { sessionId: string; skipStale?: boolean } = { sessionId }
|
|
86
|
+
if (skipStale) payload.skipStale = true
|
|
87
|
+
|
|
88
|
+
return rpc('edrv.list', payload)
|
|
89
|
+
.then((res) => {
|
|
90
|
+
if (current !== entry.seq) return
|
|
91
|
+
if (!res || !res.ok || !Array.isArray(res.records)) return
|
|
92
|
+
entry.records = res.records as RecordView[]
|
|
93
|
+
notifyRecords(entry)
|
|
94
|
+
})
|
|
95
|
+
.catch((e) => {
|
|
96
|
+
// 拉取失败保留上一次快照,等下一轮心跳重试;同时把错误交给关心者(EditorView 报错条)。
|
|
97
|
+
if (current !== entry.seq) return
|
|
98
|
+
const message = String(e)
|
|
99
|
+
for (const listener of Array.from(entry.errorListeners)) {
|
|
100
|
+
try {
|
|
101
|
+
listener(message)
|
|
102
|
+
} catch {
|
|
103
|
+
// 单个错误订阅者异常不得影响其余订阅者。
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
.finally(() => {
|
|
108
|
+
if (current === entry.seq) entry.inflight = false
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 确保该会话的定时器与 refresh 事件监听已就绪(首个订阅者到来时启动)。
|
|
114
|
+
* @author ddj 2026年10月19号
|
|
115
|
+
* @param sessionId 会话 id
|
|
116
|
+
* @param entry 订阅槽位
|
|
117
|
+
*/
|
|
118
|
+
function ensureTimer(sessionId: string, entry: FeedEntry): void {
|
|
119
|
+
if (entry.timer === null) {
|
|
120
|
+
entry.timer = setInterval(() => {
|
|
121
|
+
const tickers = Array.from(entry.tickers)
|
|
122
|
+
for (const tick of tickers) {
|
|
123
|
+
try {
|
|
124
|
+
tick()
|
|
125
|
+
} catch {
|
|
126
|
+
// 心跳订阅者异常不得中断定时器。
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 只有存在记录/错误订阅者时才拉取记录;纯心跳订阅者(如体积徽章)不额外制造 RPC。
|
|
130
|
+
if (entry.listeners.size > 0 || entry.errorListeners.size > 0) void refreshFeed(sessionId)
|
|
131
|
+
}, FEED_INTERVAL_MS)
|
|
132
|
+
}
|
|
133
|
+
if (entry.onRefresh === null) {
|
|
134
|
+
const handler = () => {
|
|
135
|
+
void refreshFeed(sessionId)
|
|
136
|
+
}
|
|
137
|
+
const target = win()
|
|
138
|
+
if (target) target.addEventListener('edrv:refresh', handler)
|
|
139
|
+
entry.onRefresh = handler
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 会话无任何订阅者时释放定时器、事件监听与缓存快照。
|
|
145
|
+
* @author ddj 2026年10月19号
|
|
146
|
+
* @param sessionId 会话 id
|
|
147
|
+
* @param entry 订阅槽位
|
|
148
|
+
*/
|
|
149
|
+
function releaseIfIdle(sessionId: string, entry: FeedEntry): void {
|
|
150
|
+
if (entry.listeners.size || entry.tickers.size || entry.errorListeners.size) return
|
|
151
|
+
if (entry.timer !== null) clearInterval(entry.timer)
|
|
152
|
+
if (entry.onRefresh !== null) {
|
|
153
|
+
const target = win()
|
|
154
|
+
if (target) target.removeEventListener('edrv:refresh', entry.onRefresh)
|
|
155
|
+
}
|
|
156
|
+
feeds.delete(sessionId)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 订阅会话的差异记录。
|
|
161
|
+
* 首个订阅者触发一次立即拉取;同会话多订阅者共享一次 RPC。
|
|
162
|
+
* @author ddj 2026年10月19号
|
|
163
|
+
* @param sessionId 会话 id
|
|
164
|
+
* @param listener 记录变化回调
|
|
165
|
+
* @returns 取消订阅函数
|
|
166
|
+
*/
|
|
167
|
+
export function subscribeRecords(sessionId: string, listener: RecordsListener): () => void {
|
|
168
|
+
const entry = entryOf(sessionId)
|
|
169
|
+
entry.listeners.add(listener)
|
|
170
|
+
ensureTimer(sessionId, entry)
|
|
171
|
+
|
|
172
|
+
if (entry.records) listener(entry.records)
|
|
173
|
+
else if (!entry.inflight) void refreshFeed(sessionId)
|
|
174
|
+
|
|
175
|
+
return () => {
|
|
176
|
+
entry.listeners.delete(listener)
|
|
177
|
+
releaseIfIdle(sessionId, entry)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 订阅会话的周期心跳(复用同一定时器,不额外制造定时器)。
|
|
183
|
+
* @author ddj 2026年10月19号
|
|
184
|
+
* @param sessionId 会话 id
|
|
185
|
+
* @param listener 心跳回调
|
|
186
|
+
* @returns 取消订阅函数
|
|
187
|
+
*/
|
|
188
|
+
export function subscribeTick(sessionId: string, listener: TickListener): () => void {
|
|
189
|
+
const entry = entryOf(sessionId)
|
|
190
|
+
entry.tickers.add(listener)
|
|
191
|
+
ensureTimer(sessionId, entry)
|
|
192
|
+
|
|
193
|
+
return () => {
|
|
194
|
+
entry.tickers.delete(listener)
|
|
195
|
+
releaseIfIdle(sessionId, entry)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 订阅会话记录拉取失败(错误提示用;同样复用该会话的共享通道)。
|
|
201
|
+
* @author ddj 2026年10月19号
|
|
202
|
+
* @param sessionId 会话 id
|
|
203
|
+
* @param listener 失败回调,参数为已字符串化的错误
|
|
204
|
+
* @returns 取消订阅函数
|
|
205
|
+
*/
|
|
206
|
+
export function subscribeErrors(sessionId: string, listener: ErrorListener): () => void {
|
|
207
|
+
const entry = entryOf(sessionId)
|
|
208
|
+
entry.errorListeners.add(listener)
|
|
209
|
+
ensureTimer(sessionId, entry)
|
|
210
|
+
|
|
211
|
+
return () => {
|
|
212
|
+
entry.errorListeners.delete(listener)
|
|
213
|
+
releaseIfIdle(sessionId, entry)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 读取会话的最近一次记录快照。
|
|
219
|
+
* @author ddj 2026年10月19号
|
|
220
|
+
* @param sessionId 会话 id
|
|
221
|
+
* @returns 记录数组或 null(尚未拉到过)
|
|
222
|
+
*/
|
|
223
|
+
export function readRecords(sessionId: string): RecordView[] | null {
|
|
224
|
+
return feeds.get(sessionId)?.records ?? null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 主动丢弃会话的共享通道(会话销毁或测试复位用)。
|
|
229
|
+
* @author ddj 2026年10月19号
|
|
230
|
+
* @param sessionId 会话 id
|
|
231
|
+
*/
|
|
232
|
+
export function disposeFeed(sessionId: string): void {
|
|
233
|
+
const entry = feeds.get(sessionId)
|
|
234
|
+
if (!entry) return
|
|
235
|
+
if (entry.timer !== null) clearInterval(entry.timer)
|
|
236
|
+
if (entry.onRefresh !== null) {
|
|
237
|
+
const target = win()
|
|
238
|
+
if (target) target.removeEventListener('edrv:refresh', entry.onRefresh)
|
|
239
|
+
}
|
|
240
|
+
feeds.delete(sessionId)
|
|
241
|
+
}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* dsh-vscode-mode client — 会话级差异 dock。
|
|
4
4
|
* conversation.input.dock 是唯一实例:普通对话显示摘要,文件编辑页显示完整操作条。
|
|
5
|
+
* 轮询已收敛到 state/recordsFeed.ts 的会话级共享订阅,本组件不再自建定时器。
|
|
5
6
|
* 作者 ddj 2026-08-26
|
|
6
7
|
*/
|
|
7
8
|
import React from 'react'
|
|
8
|
-
import { rpc } from '../rpc.js'
|
|
9
9
|
import { openDiffView } from '../events.js'
|
|
10
10
|
import { summarize } from '../state/records.js'
|
|
11
|
+
import { subscribeRecords } from '../state/recordsFeed.js'
|
|
11
12
|
import { nextDiffPath } from '../diffDock.js'
|
|
12
13
|
import { dockInLayout, readDiffDock, subscribeDiffDock } from '../diffDockStore.js'
|
|
13
14
|
import { DiffBox } from './DiffBox.js'
|
|
@@ -25,27 +26,17 @@ export function ConversationDiffDock(props) {
|
|
|
25
26
|
const sessionId = props?.sessionId
|
|
26
27
|
const [summary, setSummary] = React.useState(null)
|
|
27
28
|
const [, setStoreRevision] = React.useState(0)
|
|
28
|
-
const seq = React.useRef(0)
|
|
29
|
-
|
|
30
|
-
const load = React.useCallback(() => {
|
|
31
|
-
if (!sessionId) return
|
|
32
|
-
const current = ++seq.current
|
|
33
|
-
rpc('edrv.list', { sessionId }).then((res) => {
|
|
34
|
-
if (current !== seq.current || !res || !res.ok || !Array.isArray(res.records)) return
|
|
35
|
-
setSummary(summarize(res.records))
|
|
36
|
-
}).catch(() => {})
|
|
37
|
-
}, [sessionId])
|
|
38
29
|
|
|
39
30
|
React.useEffect(() => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
window.addEventListener('edrv:refresh', onRefresh)
|
|
44
|
-
return () => {
|
|
45
|
-
clearInterval(timer)
|
|
46
|
-
window.removeEventListener('edrv:refresh', onRefresh)
|
|
31
|
+
if (!sessionId) {
|
|
32
|
+
setSummary(null)
|
|
33
|
+
return undefined
|
|
47
34
|
}
|
|
48
|
-
|
|
35
|
+
|
|
36
|
+
return subscribeRecords(sessionId, (records) => {
|
|
37
|
+
setSummary(summarize(records))
|
|
38
|
+
})
|
|
39
|
+
}, [sessionId])
|
|
49
40
|
|
|
50
41
|
React.useEffect(() => {
|
|
51
42
|
if (!sessionId) return undefined
|
|
@@ -1,39 +1,32 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
3
|
* dsh-vscode-mode client — DiffBadge:header 差异角标(仅工作区有差异时显示)。
|
|
4
|
-
*
|
|
4
|
+
* 轮询已收敛到 state/recordsFeed.ts 的会话级共享订阅,本组件不再自建定时器。
|
|
5
5
|
* 作者 ddj 2026-08-20
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react'
|
|
8
8
|
import { appUrl } from '../../shared/appUrl.js'
|
|
9
|
-
import { rpc } from '../rpc.js'
|
|
10
9
|
import { openEditorView, emitShowLauncher } from '../events.js'
|
|
11
10
|
import { summarize } from '../state/records.js'
|
|
11
|
+
import { subscribeRecords } from '../state/recordsFeed.js'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* 会话 header
|
|
14
|
+
* 会话 header 差异角标:订阅会话差异记录,有差异时显示数字徽章,点击打开编辑区并拉起 DiffLauncher。
|
|
15
15
|
*/
|
|
16
16
|
export function DiffBadge(props) {
|
|
17
17
|
const sessionId = props?.sessionId
|
|
18
18
|
const [summary, setSummary] = React.useState(null)
|
|
19
|
-
const seq = React.useRef(0)
|
|
20
|
-
|
|
21
|
-
const load = React.useCallback(() => {
|
|
22
|
-
if (!sessionId) return
|
|
23
|
-
const s = ++seq.current
|
|
24
|
-
rpc('edrv.list', { sessionId }).then((res) => {
|
|
25
|
-
if (s !== seq.current || !res || !res.ok || !Array.isArray(res.records)) return
|
|
26
|
-
setSummary(summarize(res.records))
|
|
27
|
-
}).catch(() => {})
|
|
28
|
-
}, [sessionId])
|
|
29
19
|
|
|
30
20
|
React.useEffect(() => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
if (!sessionId) {
|
|
22
|
+
setSummary(null)
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return subscribeRecords(sessionId, (records) => {
|
|
27
|
+
setSummary(summarize(records))
|
|
28
|
+
})
|
|
29
|
+
}, [sessionId])
|
|
37
30
|
|
|
38
31
|
const n = summary?.totalFiles ?? 0
|
|
39
32
|
if (!n) return null
|