dsh-vscode-mode 0.17.0 → 0.19.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 +8 -4
- package/lib/client.debug.js +934 -0
- package/lib/client.debug.js.map +1 -0
- package/lib/client.js +1425 -1724
- package/lib/client.js.map +1 -1
- package/lib/client.rules.js +793 -0
- package/lib/client.rules.js.map +1 -0
- package/lib/index.js +1078 -286
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/dsh-vscodemode-mcp/SKILL.md +24 -7
- package/src/client/index.ts +24 -0
- package/src/client/lazyPanel.ts +68 -0
- package/src/client/mcpSessionState.ts +108 -0
- package/src/client/monaco/lsp/providers.ts +32 -3
- package/src/client/sidebar/PanelBoundary.ts +57 -0
- package/src/client/sidebar/SidebarView.ts +58 -21
- package/src/client/sidebar/panels/FileExplorer.ts +80 -27
- package/src/client/sidebar/panels/SvnPanel.ts +24 -8
- package/src/client/sidebar/panels/index.ts +3 -4
- package/src/client/sidebar/panels/treeWindow.ts +106 -0
- package/src/client/sidebar/panels/useExplorerReveal.ts +5 -2
- package/src/client/sidebar/registry.ts +4 -1
- package/src/client/state/modelWarmer.ts +198 -0
- package/src/client/styles/editor.css +13 -1
- package/src/client/textPages.ts +135 -0
- package/src/client/ui/EditorPane.ts +16 -4
- package/src/client/ui/EditorView.ts +147 -29
- package/src/client/ui/LargeTextView.ts +94 -0
- package/src/client/ui/McpSessionStatus.ts +75 -0
- package/src/client/ui/McpSettings.ts +47 -13
- package/src/mcp.ts +1 -0
- package/src/mcpBridge.ts +163 -0
- package/src/mcpDiscover.ts +38 -0
- package/src/mcpPool.ts +245 -0
- package/src/mcpProject.ts +15 -12
- package/src/mcpRuntime.ts +299 -254
- package/src/rpc.ts +42 -8
- package/src/shared/mcp.ts +47 -0
- package/src/shared/rpc.ts +8 -2
- package/src/shared/textPages.ts +28 -0
- package/src/textPages.ts +149 -0
- package/src/treeIndex.ts +62 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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": [
|
|
@@ -14,8 +14,8 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| 持久配置 | profile loader 树 | 已注册 workspace 根目录的 `.mcp.json` |
|
|
16
16
|
| 生效范围 | profile 内所有工作区 | cwd 匹配该 workspace 的 agent |
|
|
17
|
-
| 运行方式 | 原有全局 loader entry |
|
|
18
|
-
| 连接实例 | 每个启用的全局 entry 一份 |
|
|
17
|
+
| 运行方式 | 原有全局 loader entry | 私有连接 scope + 会话授权代理,复用官方 MCP 插件 |
|
|
18
|
+
| 连接实例 | 每个启用的全局 entry 一份 | 默认逐 agent 隔离;显式选择 workspace 后工作区复用 |
|
|
19
19
|
| 没有活动 agent | 仍可运行 | 仅保存配置,`configured`、`instanceCount: 0` |
|
|
20
20
|
| 团队共享 | 否 | `.mcp.json` 可以随仓库共享 |
|
|
21
21
|
|
|
@@ -25,7 +25,9 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
25
25
|
|
|
26
26
|
设置 → VSCodeMode → MCP 管理,包含「我的 MCP」「项目 MCP」「MCP 市场」(占位)。项目服务在所选项目内管理。
|
|
27
27
|
|
|
28
|
-
保存项目服务会写入 `.mcp.json`
|
|
28
|
+
保存项目服务会写入 `.mcp.json` 并更新匹配 agent 的能力目录与惰性资源入口。新建会话只等待配置准备和轻量入口,不等待任何项目 MCP 握手;默认 `on-demand`,未使用不启动。显式 `background` 后台预热也不阻塞创建。同一 Host 最多两个冷启动;同代次初始化 single-flight。配置写操作仍串行保护,但连接准备不占有跨会话全局锁。
|
|
29
|
+
|
|
30
|
+
AI 先调用全局发现入口 `discover_project_mcp {action:"list"}`,仅返回执行 agent 工作区内的安全目录;需要时 `{action:"connect",serverName:"..."}` 连接并返回真实 schema。入口从工具执行上下文取 agent,不接受 sessionId、路径、命令、URL 或凭据。真实工具依然走官方审批、guard 和结果投影;Native 在下一次请求获得工具,PTC 必须结束当前程序后新建程序刷新 SDK,禁止 eval 或通用透传绕过权限。资源工具可通过所属工作区的惰性 provider 按需连接。
|
|
29
31
|
|
|
30
32
|
禁用、删除、配置变更、刷新重连及 agent/插件卸载都会释放相应 fiber 和连接。仅刷新未变化的项目列表不会重复连接。主机代码更新需要重启 DSH 才能加载新实现。
|
|
31
33
|
|
|
@@ -33,6 +35,14 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
33
35
|
|
|
34
36
|
表单 stdio 参数采用 JSON `string[]` 或每行一个完整参数,不按空格/引号做 shell 拆分;含空参数或内嵌换行使用 JSON。HTTP 请求头每行 `key=value`,保留值内的等号,拒绝非法/重复(忽略大小写)键。失败保存保持表单,忙时禁止重复动作。
|
|
35
37
|
|
|
38
|
+
### 会话列表状态
|
|
39
|
+
|
|
40
|
+
官方 `sidebar.session.row.leading` 插槽在会话标题前显示 16px 加载或异常图标;平稳完成不常驻绿色成功标。官方运行中、待审批、新消息状态优先覆盖 leading,归档行不展示 leading;`sidebar.session.row.hover` 悬浮详情仍可查看。
|
|
41
|
+
|
|
42
|
+
详情只描述 VSCodeMode 管理的**项目 MCP**:待使用(不转圈)、读取配置、等待启动名额、加载插件模块、等待服务插件启动、复用已有连接、释放旧实例、完成或异常,展示完成数、耗时与安全错误摘要。共享显示会话本次等待,不将连接年龄冒充本次等待。超过 10 秒仅提示耗时较长,不判定失败。完成文案为「初始化完成,连接健康未验证」。无活跃 agent 文案为「当前无活跃 MCP 实例」,不是成功;不包含全局或 preset MCP。
|
|
43
|
+
|
|
44
|
+
所有可见行共用一份 `mcp.sessionSnapshot` 订阅,在文档可见且有消费者时按 2.5 秒间隔查询;请求 10 秒截止、不重叠,隐藏/退订/卸载取消,过期响应丢弃。错误保留上次快照并标过期。查询只读内存,**不读配置、不 reconcile、不激活/保留历史会话、不建立连接**;旧 Host 或缺少插槽时安全降级。错误详情不返回 env、headers、完整配置或凭据 URL,仅返回安全类别提示,原始诊断看 Host 日志。
|
|
45
|
+
|
|
36
46
|
## 3. 项目配置
|
|
37
47
|
|
|
38
48
|
`.mcp.json` 示例:
|
|
@@ -58,6 +68,8 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
58
68
|
- stdio:`command` 必填;可填 `args`、`cwd`、`env`。未指定 `cwd` 时,以项目根作为进程工作目录。
|
|
59
69
|
- HTTP:`url` 必须以 `http://` 或 `https://` 开头,可填 `headers`。
|
|
60
70
|
- 高级参数:`toolCallTimeoutMs`、`failOnStartupError`、`maxInstructionBytes`、`reconnect`。
|
|
71
|
+
- 项目扩展 `vscodeMode: { startup: "on-demand" | "background", sharing: "agent" | "workspace", startupTimeoutMs: 60000 }` 与官方传输配置分离,不透传;默认 on-demand + agent,旧文件不批量改写。初始化预算接受 1000–300000 整数,不是业务调用超时。全局配置不接受项目扩展。
|
|
72
|
+
- workspace 共享必须显式确认服务适合共享内部状态;不会自动给 CodeGraph 开启。池键含工作区、有效配置和策略,凭据只做内存摘要身份。共享调用默认串行,最后租约释放保温 60 秒;agent 模式立即释放。禁用/删除/刷新/配置变化先失效与撤销再关闭,关闭未确认则阻止替代。超时显示释放阶段,失败冷却 5 秒后允许显式重试,不无限自动重试。
|
|
61
73
|
- `disabled: true` 保持配置但不创建连接;保存表单会恢复启用。重启和 reconcile 都尊重 `disabled`。
|
|
62
74
|
- 保存保留文档顶层、其他服务器及当前服务器的未知字段和未编辑高级参数;切换传输方式会移除旧传输专属字段。
|
|
63
75
|
- `env` / `headers` 在返回值中使用 `••••••` 脱敏。原键的未修改掩码保存时恢复真实值;新键只有掩码而没有原值时会拒绝保存,需填写凭据。
|
|
@@ -85,12 +97,14 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
85
97
|
|---|---|---|
|
|
86
98
|
| `mcp.list` | `{}` | `servers` |
|
|
87
99
|
| `mcp.snapshot` | `{}` | `servers`, `projects`(只读运行快照) |
|
|
100
|
+
| `mcp.sessionSnapshot` | `{}` | `sessions`, `sampledAt`(按 sessionId 区分的活跃初始化状态,只读内存) |
|
|
88
101
|
| `mcp.save` | `{ config }` | `server` |
|
|
89
102
|
| `mcp.remove` | `{ id }` | 无 |
|
|
90
103
|
| `mcp.toggle` | `{ id, enabled }` | `server` |
|
|
91
104
|
| `mcp.refresh` | `{ id }` | `server` |
|
|
92
105
|
| `mcp.projects` | `{}` | `projects` |
|
|
93
|
-
| `mcp.projectSave` | `{ workspacePath, serverName, config }` | `project` |
|
|
106
|
+
| `mcp.projectSave` | `{ workspacePath, serverName, config, policy? }` | `project` |
|
|
107
|
+
| `mcp.projectConnect` | `{ sessionId, workspacePath, serverName }` | `project`(仅已活跃匹配会话,不激活历史会话) |
|
|
94
108
|
| `mcp.projectRemove` | `{ workspacePath, serverName }` | `project` |
|
|
95
109
|
| `mcp.projectToggle` | `{ workspacePath, serverName, enabled }` | `project` |
|
|
96
110
|
| `mcp.projectRefresh` | `{ workspacePath, serverName }` | `project` |
|
|
@@ -109,7 +123,7 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
109
123
|
|
|
110
124
|
## 6. 工具、资源和指令隔离
|
|
111
125
|
|
|
112
|
-
官方 0.1.7-rc.2 的 MCP Config 没有关闭资源/指令的开关;官方注册随 `scopeOf(ctx)`
|
|
126
|
+
官方 0.1.7-rc.2 的 MCP Config 没有关闭资源/指令的开关;官方注册随 `scopeOf(ctx)` 归属。因此连接在独立 scope 中隔离 tools、mcpResources、systemPrompt,捕获注册后再给合法 agent 提供代理,绝不将原始项目注册暴露全局。创建私有 scope 后先等待 backing fiber 初始激活完成再 provide,防止依赖服务丢失;真实官方模块测试覆盖此边界。
|
|
113
127
|
|
|
114
128
|
子 agent 会继承父 scope;跨 cwd 子会话还有三层处理:
|
|
115
129
|
|
|
@@ -126,7 +140,7 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
126
140
|
|
|
127
141
|
| 状态/现象 | 含义与处理 |
|
|
128
142
|
|---|---|
|
|
129
|
-
| `configured` |
|
|
143
|
+
| `configured` | 中性色;配置已保存,尚无连接实例,首次发现/资源访问或显式手动连接才启动(background 策略除外) |
|
|
130
144
|
| `disabled` | 配置已停用,不建立项目连接 |
|
|
131
145
|
| `connecting` | 官方插件还在等待启动完成;检查命令、cwd、URL 和握手 |
|
|
132
146
|
| `unverified` | 中性色;fiber 已激活,但官方没有公开连接状态证据,不能宣称已连接;右侧绿色开关只表示启用 |
|
|
@@ -140,7 +154,10 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
140
154
|
|
|
141
155
|
## 8. 维护位置与验证
|
|
142
156
|
|
|
143
|
-
- `src/mcpRuntime.ts
|
|
157
|
+
- `src/mcpRuntime.ts`:会话租约/能力台账、官方模块加载、非阻塞接入、迁移与卸载。
|
|
158
|
+
- `src/mcpPool.ts`:独立连接、single-flight、并发名额、取消/超时/保温和代次围栏。
|
|
159
|
+
- `src/mcpBridge.ts`:私有服务隔离、真实工具代理、动态列表和撤销。
|
|
160
|
+
- `src/mcpDiscover.ts`:统一发现入口与按 scope 输出的能力目录。
|
|
144
161
|
- `src/mcpProject.ts`:源配置读取、保留字段、命名检查及 reconcile。
|
|
145
162
|
- `src/mcpIsolation.ts`:继承工具限制、资源执行 guard、指令和资源列表覆盖。
|
|
146
163
|
- `src/mcp.ts`:全局 loader CRUD、脱敏与诚实状态。
|
package/src/client/index.ts
CHANGED
|
@@ -20,6 +20,8 @@ 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 { mcpSessionState } from './mcpSessionState.js'
|
|
24
|
+
import { McpSessionIcon, McpSessionDetail, type McpRowProps } from './ui/McpSessionStatus.js'
|
|
23
25
|
import { PluginDetailSection } from './ui/PluginDetailSection.js'
|
|
24
26
|
import { rpc } from './rpc.js'
|
|
25
27
|
import { loadMonaco } from './monaco/loader.js'
|
|
@@ -74,6 +76,14 @@ import { flushPageState, flushUiState, hydrateUiState, installUiStatePersist, on
|
|
|
74
76
|
import { workspaceScopeOf } from './state/scopeStore.js'
|
|
75
77
|
import { dapStore } from './dap/store.js'
|
|
76
78
|
import type { CompatAdapter } from '../shared/compat.js'
|
|
79
|
+
import { setPanelImport } from './lazyPanel.js'
|
|
80
|
+
import { dbg } from './rpc.js'
|
|
81
|
+
import { BP_MODE, MODES, fieldOfMode } from './dap/BpWidget.js'
|
|
82
|
+
import { ContextMenu } from './ui/ContextMenu.js'
|
|
83
|
+
|
|
84
|
+
/** Root-owned live objects shared with independent panel chunks; no copied stores or transport state. */
|
|
85
|
+
export const __lazyShared = { rpc, dbg, dapStore, BP_MODE, MODES, fieldOfMode, ContextMenu }
|
|
86
|
+
export { setPanelImport as __setPanelImport }
|
|
77
87
|
|
|
78
88
|
// ⚠️ inject 只列必需服务:webUiSettings 是 @linxin666/dsh-client-ui-web-ui-settings 提供的
|
|
79
89
|
// 可选兼容桥(非官方服务),列入 inject 会让未装该桥的部署启动时 entry 永久 pending,
|
|
@@ -634,6 +644,18 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
634
644
|
}
|
|
635
645
|
retrySideService()
|
|
636
646
|
|
|
647
|
+
// Root row slots receive only sessionId: no sessions.scope()/activation for historical rows.
|
|
648
|
+
ctx.effect(() => {
|
|
649
|
+
const store = mcpSessionState()
|
|
650
|
+
/** @author ddj 2026年10月20号 @param props Official root row identity. @returns Leading marker with shared polling. */
|
|
651
|
+
const renderIcon = (props: unknown) => React.createElement(McpSessionIcon, { ...(props as McpRowProps), store })
|
|
652
|
+
/** @author ddj 2026年10月20号 @param props Official hover identity. @returns Project-only initialization details. */
|
|
653
|
+
const renderDetail = (props: unknown) => React.createElement(McpSessionDetail, { ...(props as McpRowProps), store })
|
|
654
|
+
const leading = registerSlotSafely(ctx, { name: 'sidebar.session.row.leading', id: 'edrv-mcp-status', order: 20 }, renderIcon)
|
|
655
|
+
const hover = registerSlotSafely(ctx, { name: 'sidebar.session.row.hover', id: 'edrv-mcp-detail', order: 20 }, renderDetail)
|
|
656
|
+
return () => { leading?.(); hover?.(); store.dispose() }
|
|
657
|
+
}, 'vscode-mode: session MCP status')
|
|
658
|
+
|
|
637
659
|
// 对话输入框上方差异 dock:普通对话显示单文案按钮,文件编辑页由 EditorView 隐藏
|
|
638
660
|
registerSlotSafely(ctx, {
|
|
639
661
|
name: 'conversation.input.dock',
|
|
@@ -659,6 +681,7 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
659
681
|
label: () => t('slot.settings'),
|
|
660
682
|
}, () => React.createElement(SettingsContext.Provider, { value: bridge.scope() }, React.createElement(McpSettings, {
|
|
661
683
|
openerRegistry: registry,
|
|
684
|
+
getMcpSession: () => readSessionScope(ctx).sessionId,
|
|
662
685
|
compatSummary,
|
|
663
686
|
// 快捷键统一走 DSH 官方配置:通用页只提供「打开官方快捷键配置」入口。
|
|
664
687
|
// 服务运行时探测(自愈):旧版 DSH 无该服务时按钮给出手动打开方式。
|
|
@@ -677,6 +700,7 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
677
700
|
// 注销挂在存活的 window.monaco 上的全部 Monaco provider 并复位模块状态。
|
|
678
701
|
// 不做会导致重载后重复注册(补全/跳转/hover 各翻倍)与陈旧开关残留。
|
|
679
702
|
ctx.effect(() => () => {
|
|
703
|
+
setPanelImport()
|
|
680
704
|
try { disposeSnippets() } catch { /* 卸载异常不得阻断其余清理 */ }
|
|
681
705
|
try { disposeLaunchJson() } catch { /* 同上 */ }
|
|
682
706
|
try { disposeAiInline() } catch { /* 同上 */ }
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Package-local CJS panel loading; transport, revision URLs and HMR belong to DSH. @author ddj 2026年10月22号 */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import type { SidebarCtx } from './sidebar/types.js'
|
|
4
|
+
|
|
5
|
+
type Panel = React.ComponentType<{ ctx: SidebarCtx }>
|
|
6
|
+
type PanelKey = 'rules' | 'debug'
|
|
7
|
+
type PanelImport = (file: string) => Promise<Record<string, unknown>>
|
|
8
|
+
interface PanelLoad { pending?: Promise<Panel>; component?: Panel }
|
|
9
|
+
const FILES = { rules: './client.rules.js', debug: './client.debug.js' } as const
|
|
10
|
+
const EXPORTS = { rules: 'RulesPanel', debug: 'DebugPanel' } as const
|
|
11
|
+
const loads = new Map<PanelKey, PanelLoad>()
|
|
12
|
+
let generation = 0
|
|
13
|
+
let importer: PanelImport | undefined
|
|
14
|
+
|
|
15
|
+
/** @public @author ddj 2026年10月22号 @param transport Injected ModuleLoader require.async, never a global require. @description Start a new plugin generation and discard older results. */
|
|
16
|
+
export function setPanelImport(transport?: PanelImport): void {
|
|
17
|
+
generation++
|
|
18
|
+
loads.clear()
|
|
19
|
+
importer = transport
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @public @author ddj 2026年10月22号 @param key Fixed built-in panel key. @returns Shared pending load or cached component. @throws Unsupported loader or invalid chunk export. */
|
|
23
|
+
export function loadPanel(key: PanelKey): Promise<Panel> {
|
|
24
|
+
const cached = loads.get(key)
|
|
25
|
+
if (cached?.component) return Promise.resolve(cached.component)
|
|
26
|
+
if (cached?.pending) return cached.pending
|
|
27
|
+
if (!importer) return Promise.reject(new Error('当前 DSH 不支持包内面板加载,请升级宿主'))
|
|
28
|
+
|
|
29
|
+
const epoch = generation
|
|
30
|
+
const transport = importer
|
|
31
|
+
const state: PanelLoad = {}
|
|
32
|
+
const pending = Promise.resolve().then(() => transport(FILES[key])).then((exports) => {
|
|
33
|
+
const component = exports[EXPORTS[key]]
|
|
34
|
+
if (typeof component !== 'function') throw new Error('面板脚本缺少组件导出')
|
|
35
|
+
if (epoch !== generation) throw new Error('面板加载已失效,请重试')
|
|
36
|
+
state.component = component as Panel
|
|
37
|
+
return state.component
|
|
38
|
+
}).catch((error) => {
|
|
39
|
+
if (loads.get(key) === state) loads.delete(key)
|
|
40
|
+
throw error
|
|
41
|
+
})
|
|
42
|
+
state.pending = pending
|
|
43
|
+
loads.set(key, state)
|
|
44
|
+
return pending
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @public @author ddj 2026年10月22号 @param props Fixed panel and current context. @returns Loading, explicit retry, or the loaded panel subtree. */
|
|
48
|
+
export function LazyPanel(props: { panel: PanelKey; ctx: SidebarCtx }): React.ReactElement {
|
|
49
|
+
const [attempt, retry] = React.useState(0)
|
|
50
|
+
const [view, setView] = React.useState<{ component?: Panel; error?: string }>({})
|
|
51
|
+
/** @private @author ddj 2026年10月22号 @returns Cleanup invalidating callbacks after switch or unmount. */
|
|
52
|
+
function loadEffect(): () => void {
|
|
53
|
+
let active = true
|
|
54
|
+
setView({})
|
|
55
|
+
/** @author ddj 2026年10月22号 @param component Loaded panel. */
|
|
56
|
+
function ready(component: Panel): void { if (active) setView({ component }) }
|
|
57
|
+
/** @author ddj 2026年10月22号 @param error Loader failure. */
|
|
58
|
+
function failed(error: unknown): void { if (active) setView({ error: String(error) }) }
|
|
59
|
+
void loadPanel(props.panel).then(ready, failed)
|
|
60
|
+
return () => { active = false }
|
|
61
|
+
}
|
|
62
|
+
React.useEffect(loadEffect, [props.panel, attempt])
|
|
63
|
+
/** @private @author ddj 2026年10月22号 @description Explicit retry; failures never loop automatically. */
|
|
64
|
+
function onRetry(): void { retry(attempt + 1) }
|
|
65
|
+
if (view.component) return React.createElement(view.component, { ctx: props.ctx })
|
|
66
|
+
return React.createElement('div', { className: 'edrv-empty', role: view.error ? 'alert' : 'status' },
|
|
67
|
+
view.error ?? '正在加载面板…', view.error && React.createElement('button', { onClick: onRetry }, '重试'))
|
|
68
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Shared, read-only sidebar MCP polling. @author ddj 2026年10月20号 */
|
|
2
|
+
import { rpc } from './rpc.js'
|
|
3
|
+
import type { McpSessionStatus } from '../shared/mcp.js'
|
|
4
|
+
|
|
5
|
+
export interface SessionMcpView { status?: McpSessionStatus; stale: boolean }
|
|
6
|
+
type Snapshot = { sessions: McpSessionStatus[]; sampledAt: number }
|
|
7
|
+
type FetchStatus = (signal: AbortSignal) => Promise<Snapshot>
|
|
8
|
+
const unavailable: SessionMcpView = { stale: true }
|
|
9
|
+
const inactive: SessionMcpView = { stale: false }
|
|
10
|
+
|
|
11
|
+
/** One poller for all mounted row/hover consumers; never retains or activates sessions. */
|
|
12
|
+
export class McpSessionState {
|
|
13
|
+
private views = new Map<string, SessionMcpView>()
|
|
14
|
+
private listeners = new Map<string, Set<() => void>>()
|
|
15
|
+
private timer?: ReturnType<typeof setTimeout>
|
|
16
|
+
private request?: AbortController
|
|
17
|
+
private epoch = 0
|
|
18
|
+
private stale = true
|
|
19
|
+
private disposed = false
|
|
20
|
+
|
|
21
|
+
/** @public @author ddj 2026年10月20号 @param fetchStatus Authenticated read-only query. @param page Visibility boundary, omitted for server tests. */
|
|
22
|
+
constructor(private readonly fetchStatus: FetchStatus, private readonly page?: Document) {}
|
|
23
|
+
|
|
24
|
+
/** @public @author ddj 2026年10月20号 @param sessionId Row identity. @returns Stable view until the next observation. */
|
|
25
|
+
get(sessionId: string): SessionMcpView { return this.views.get(sessionId) ?? (this.stale ? unavailable : inactive) }
|
|
26
|
+
|
|
27
|
+
/** @public @author ddj 2026年10月20号 @param sessionId Row identity. @param listener React invalidation. @returns Idempotent unsubscribe; the last consumer stops polling. */
|
|
28
|
+
subscribe(sessionId: string, listener: () => void): () => void {
|
|
29
|
+
if (this.disposed) return () => undefined
|
|
30
|
+
const first = this.listeners.size === 0
|
|
31
|
+
const listeners = this.listeners.get(sessionId) ?? new Set()
|
|
32
|
+
listeners.add(listener)
|
|
33
|
+
this.listeners.set(sessionId, listeners)
|
|
34
|
+
if (first) {
|
|
35
|
+
this.page?.addEventListener('visibilitychange', this.visibility)
|
|
36
|
+
void this.poll()
|
|
37
|
+
}
|
|
38
|
+
return () => {
|
|
39
|
+
listeners.delete(listener)
|
|
40
|
+
if (listeners.size === 0) this.listeners.delete(sessionId)
|
|
41
|
+
if (this.listeners.size === 0) this.pause()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @private @author ddj 2026年10月20号 Stops pending work when hidden; resuming never accepts an old response. */
|
|
46
|
+
private visibility = (): void => {
|
|
47
|
+
if (this.page?.hidden) this.invalidate()
|
|
48
|
+
else void this.poll()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @private @author ddj 2026年10月20号 Invalidates responses without allowing overlapping transport requests. */
|
|
52
|
+
private invalidate(): void {
|
|
53
|
+
this.epoch += 1
|
|
54
|
+
clearTimeout(this.timer)
|
|
55
|
+
this.timer = undefined
|
|
56
|
+
this.request?.abort()
|
|
57
|
+
this.publish(undefined)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @private @author ddj 2026年10月20号 Cleans listener lifetime as well as timers. */
|
|
61
|
+
private pause(): void {
|
|
62
|
+
this.page?.removeEventListener('visibilitychange', this.visibility)
|
|
63
|
+
this.invalidate()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @public @author ddj 2026年10月20号 Permanently disposes HMR-owned polling. */
|
|
67
|
+
dispose(): void { this.disposed = true; this.pause(); this.listeners.clear(); this.views.clear() }
|
|
68
|
+
|
|
69
|
+
/** @private @author ddj 2026年10月20号 @param snapshot Fresh ledger, or undefined to mark all previous observations stale. */
|
|
70
|
+
private publish(snapshot?: Snapshot): void {
|
|
71
|
+
this.stale = snapshot === undefined
|
|
72
|
+
if (snapshot) this.views = new Map(snapshot.sessions.map((status) => [status.sessionId, { status, stale: false }]))
|
|
73
|
+
else for (const [id, view] of this.views) this.views.set(id, { ...view, stale: true })
|
|
74
|
+
for (const listeners of this.listeners.values()) for (const listener of listeners) listener()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @private @author ddj 2026年10月20号 Queries once, with a 10-second deadline and a 2.5-second gap after settlement. */
|
|
78
|
+
private async poll(): Promise<void> {
|
|
79
|
+
if (this.disposed || this.request || !this.listeners.size || this.page?.hidden) return
|
|
80
|
+
const request = new AbortController()
|
|
81
|
+
const epoch = this.epoch
|
|
82
|
+
this.request = request
|
|
83
|
+
/** @author ddj 2026年10月20号 Marks overdue responses unusable even if a transport ignores cancellation. */
|
|
84
|
+
const expire = (): void => { request.abort(); if (epoch === this.epoch) this.publish(undefined) }
|
|
85
|
+
const deadline = setTimeout(expire, 10000)
|
|
86
|
+
try {
|
|
87
|
+
const snapshot = await this.fetchStatus(request.signal)
|
|
88
|
+
if (epoch === this.epoch && !request.signal.aborted) this.publish(snapshot)
|
|
89
|
+
} catch {
|
|
90
|
+
if (epoch === this.epoch) this.publish(undefined)
|
|
91
|
+
} finally {
|
|
92
|
+
clearTimeout(deadline)
|
|
93
|
+
this.request = undefined
|
|
94
|
+
if (!this.disposed && this.listeners.size && !this.page?.hidden) this.timer = setTimeout(() => { void this.poll() }, 2500)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @public @author ddj 2026年10月20号 @returns Browser-owned singleton per plugin apply; legacy Hosts fail safely as stale. */
|
|
100
|
+
export function mcpSessionState(): McpSessionState {
|
|
101
|
+
/** @author ddj 2026年10月20号 @param signal Fetch lifetime. @returns Current in-memory host ledger. @throws RPC errors. */
|
|
102
|
+
const fetchStatus = async (signal: AbortSignal): Promise<Snapshot> => {
|
|
103
|
+
const result = await rpc('mcp.sessionSnapshot', {}, signal)
|
|
104
|
+
if (!result.ok) throw new Error('MCP 状态暂不可用')
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
return new McpSessionState(fetchStatus, document)
|
|
108
|
+
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* 交互层:edrv.goToDefinition(F12)/ edrv.findReferences(Shift+F12 / 右键)均走 Monaco 原生命令;
|
|
6
6
|
* Ctrl+点击使用处交给原生;Lua 表枚举字段声明处改为打开引用 Peek,
|
|
7
7
|
* 数据经 host 的 LSP RPC 提供(registerEditorOpener 接管 edrv:// 点击跳转)。
|
|
8
|
-
*
|
|
8
|
+
* 定义结果返回前预热目标 model,避免 hover 定义链接预览抛 "Model not found"。
|
|
9
|
+
* 作者 ddj 2026-08-27 / 2026-09-11 / 2026-10-20
|
|
9
10
|
*/
|
|
10
11
|
import {
|
|
11
12
|
pathOfModel, syncDoc, closeDoc, findDefinition, findReferences,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
LSP_COMPLETION_KIND_NAMES, LSP_INSERT_TEXT_FORMAT_SNIPPET,
|
|
20
21
|
} from '../../../shared/lsp.js'
|
|
21
22
|
import { luaKeywordsAt, luaTypesAt, mergeKeywords } from './keywords.js'
|
|
23
|
+
import { warmModelFor } from '../../state/modelWarmer.js'
|
|
22
24
|
|
|
23
25
|
const LSP_LANGS = ['lua', 'csharp']
|
|
24
26
|
|
|
@@ -79,6 +81,31 @@ export function navTargetOf(selectionOrPosition) {
|
|
|
79
81
|
return endColumn === undefined ? { line, column, endLine } : { line, column, endLine, endColumn }
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
/**
|
|
85
|
+
* 预热定义目标文件的 Monaco model(供 Ctrl+hover 定义链接预览使用)。
|
|
86
|
+
*
|
|
87
|
+
* 为什么需要:Monaco 的 GotoDefinitionAtPositionEditorContribution 在渲染定义链接预览时,
|
|
88
|
+
* 对目标 uri 调 `createModelReference` 后**只挂 then、没有 catch**;本插件的定义目标是
|
|
89
|
+
* 自造的 `edrv://` uri,目标文件若从未打开过就没有已注册 model,`createModelReference`
|
|
90
|
+
* 会 reject("Model not found") 变成未捕获的 promise 异常(弹到 Console)。
|
|
91
|
+
* 本 vendored Monaco 也没有 `registerTextModelContentProvider` 可惰性喂内容,故只能提前建模型。
|
|
92
|
+
*
|
|
93
|
+
* 只预热单一定义:多定义分支(`locations.length > 1`)Monaco 走列表、不调
|
|
94
|
+
* `createModelReference`,没必要读盘。预热纯属增强,任何异常都在此吞掉,
|
|
95
|
+
* 不得让跳转查询失败(F12 与 Ctrl+点击不依赖模型是否已建)。
|
|
96
|
+
* @author ddj 2026年10月20号
|
|
97
|
+
* @param locations findDefinition 的客户端结果(含 edrv uri)
|
|
98
|
+
*/
|
|
99
|
+
async function prewarmDefinitionTarget(locations) {
|
|
100
|
+
if (!Array.isArray(locations) || locations.length !== 1) return
|
|
101
|
+
try {
|
|
102
|
+
const target = lspUriToAbs(locations[0]?.uri)
|
|
103
|
+
if (target) await warmModelFor(target)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
/* 预热失败静默降级:等价于本次 hover 不显示预览 */
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
82
109
|
/** 注册全部 Monaco LSP provider 与文档跟踪(幂等)。 */
|
|
83
110
|
export function registerLspProviders(monaco) {
|
|
84
111
|
if (registered) return
|
|
@@ -151,10 +178,12 @@ export function registerLspProviders(monaco) {
|
|
|
151
178
|
},
|
|
152
179
|
}),
|
|
153
180
|
monaco.languages.registerDefinitionProvider(LSP_LANGS, {
|
|
154
|
-
provideDefinition: (model, position, token) => {
|
|
181
|
+
provideDefinition: async (model, position, token) => {
|
|
155
182
|
const path = pathOfModel(model)
|
|
156
183
|
if (!path) return []
|
|
157
|
-
|
|
184
|
+
const locations = await findDefinition(path, model.getValue(), position)
|
|
185
|
+
await prewarmDefinitionTarget(locations)
|
|
186
|
+
return locations
|
|
158
187
|
},
|
|
159
188
|
}),
|
|
160
189
|
monaco.languages.registerReferenceProvider(LSP_LANGS, {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Panel-local callback and React subtree isolation, not a security sandbox. @author ddj 2026年10月22号 */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import type { SidebarCtx, SidebarPanelDef } from './types.js'
|
|
4
|
+
|
|
5
|
+
export interface PanelCheck { panel: SidebarPanelDef; badge: number | null; error?: Error }
|
|
6
|
+
interface BoundaryProps { panel: SidebarPanelDef; scope?: string; sessionId?: string; compact?: boolean; children?: React.ReactNode }
|
|
7
|
+
interface BoundaryState { panel: SidebarPanelDef; scope?: string; sessionId?: string; error: Error | null }
|
|
8
|
+
const panelIds = new WeakMap<SidebarPanelDef, number>()
|
|
9
|
+
let nextPanelId = 0
|
|
10
|
+
/** @private @author ddj 2026年10月22号 @param props Subtree ownership. @returns Stable identity that changes on registration, scope or session replacement. */
|
|
11
|
+
function ownerKey(props: BoundaryProps): string {
|
|
12
|
+
if (!panelIds.has(props.panel)) panelIds.set(props.panel, ++nextPanelId)
|
|
13
|
+
return JSON.stringify([panelIds.get(props.panel), props.scope, props.sessionId])
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @public @author ddj 2026年10月22号 @param panel Registered definition. @param ctx Current scope. @returns Hidden marker or isolated visible/badge result; failed guards stay reachable for retry. */
|
|
17
|
+
export function inspectPanel(panel: SidebarPanelDef, ctx: SidebarCtx): PanelCheck | null {
|
|
18
|
+
try {
|
|
19
|
+
if (panel.visible && panel.visible(ctx) !== true) return null
|
|
20
|
+
return { panel, badge: panel.badge?.(ctx) ?? null }
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return { panel, badge: null, error: error instanceof Error ? error : new Error(String(error)) }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @public @author ddj 2026年10月22号 @description Reset a failed subtree when registration or scope ownership changes. */
|
|
27
|
+
export class PanelBoundary extends React.Component<BoundaryProps, BoundaryState> {
|
|
28
|
+
state: BoundaryState = { panel: this.props.panel, scope: this.props.scope, sessionId: this.props.sessionId, error: null }
|
|
29
|
+
|
|
30
|
+
/** @public @author ddj 2026年10月22号 @param props Current ownership. @param state Previous ownership/error. @returns Reset only when the owner changes. */
|
|
31
|
+
static getDerivedStateFromProps(props: BoundaryProps, state: BoundaryState): Partial<BoundaryState> | null {
|
|
32
|
+
if (state.panel === props.panel && state.scope === props.scope && state.sessionId === props.sessionId) return null
|
|
33
|
+
return { panel: props.panel, scope: props.scope, sessionId: props.sessionId, error: null }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @public @author ddj 2026年10月22号 @param error React render/lifecycle error. @returns Local fallback state. */
|
|
37
|
+
static getDerivedStateFromError(error: Error): Partial<BoundaryState> { return { error } }
|
|
38
|
+
|
|
39
|
+
/** @private @author ddj 2026年10月22号 @description Remount only this failed subtree and reevaluate its callbacks. */
|
|
40
|
+
private retry = (): void => { this.setState({ error: null }) }
|
|
41
|
+
|
|
42
|
+
/** @public @author ddj 2026年10月22号 @returns Original subtree or panel-local retry UI. */
|
|
43
|
+
render(): React.ReactNode {
|
|
44
|
+
if (!this.state.error) return React.createElement(React.Fragment, { key: ownerKey(this.props) }, this.props.children)
|
|
45
|
+
if (this.props.compact) return React.createElement('span', { role: 'img', title: '图标加载失败', 'aria-label': '图标加载失败' }, '!')
|
|
46
|
+
return React.createElement('div', { className: 'edrv-empty', role: 'alert' },
|
|
47
|
+
this.props.panel.title + ':' + String(this.state.error.message),
|
|
48
|
+
React.createElement('button', { onClick: this.retry }, '重试'))
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @public @author ddj 2026年10月22号 @param props Isolated panel invocation below the boundary. @returns Panel subtree. @throws Extension callback error for the nearest boundary. */
|
|
53
|
+
export function PanelContent(props: { panel: SidebarPanelDef; ctx: SidebarCtx }): React.ReactNode {
|
|
54
|
+
const checked = inspectPanel(props.panel, props.ctx)
|
|
55
|
+
if (checked?.error) throw checked.error
|
|
56
|
+
return checked ? props.panel.render(props.ctx) as React.ReactNode : null
|
|
57
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import React from 'react'
|
|
12
12
|
import type { SidebarCtx } from './types.js'
|
|
13
|
+
import { inspectPanel, PanelBoundary, PanelContent } from './PanelBoundary.js'
|
|
13
14
|
|
|
14
15
|
const W_MAX = 560
|
|
15
16
|
/** 面板区隐藏时容器的宽度(= 图标列宽,与 editor.css .edrv-rail 对齐)。 */
|
|
@@ -50,9 +51,8 @@ function railIconEl(icon) {
|
|
|
50
51
|
export function SidebarView(props) {
|
|
51
52
|
const ctx = props.ctx
|
|
52
53
|
// 按 visible(ctx) 过滤活动栏图标与面板(能力相关面板:SVN 变更仅在受 SVN 管理时出现)
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
))
|
|
54
|
+
const checked = (props.registry?.list?.() ?? []).map((panel) => inspectPanel(panel, ctx)).filter(Boolean)
|
|
55
|
+
const panels = checked.map((entry) => entry.panel)
|
|
56
56
|
const activePanel = props.activePanel
|
|
57
57
|
const onActive = props.onActive
|
|
58
58
|
const visible = props.visible !== false
|
|
@@ -64,27 +64,66 @@ export function SidebarView(props) {
|
|
|
64
64
|
|
|
65
65
|
const active = panels.find((p) => p.id === activePanel) || panels[0]
|
|
66
66
|
|
|
67
|
+
const dragCleanup = React.useRef(null)
|
|
68
|
+
/** @private @author ddj 2026年10月22号 @description End the active drag; safe on repeated teardown. */
|
|
69
|
+
function stopDrag() { dragCleanup.current?.() }
|
|
70
|
+
const seamOpening = React.useRef(false)
|
|
71
|
+
const lastVisible = React.useRef(visible)
|
|
72
|
+
/** @private @author ddj 2026年10月22号 @description Always release listeners on unmount, including a pending seam expansion. */
|
|
73
|
+
function dragEffect() { return stopDrag }
|
|
74
|
+
React.useEffect(dragEffect, [])
|
|
75
|
+
/** @private @author ddj 2026年10月22号 @description Preserve only the seam's own expansion; external visibility changes cancel ownership. */
|
|
76
|
+
function visibilityEffect() {
|
|
77
|
+
if (lastVisible.current !== visible) {
|
|
78
|
+
if (!(visible && seamOpening.current)) stopDrag()
|
|
79
|
+
seamOpening.current = false
|
|
80
|
+
lastVisible.current = visible
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
React.useEffect(visibilityEffect, [visible])
|
|
84
|
+
|
|
85
|
+
/** @private @author ddj 2026年10月22号 @param pointerId Owning pointer. @param move Pointer movement callback. @description Replace the previous drag and own every exit path. */
|
|
86
|
+
function bindDrag(pointerId, move) {
|
|
87
|
+
stopDrag()
|
|
88
|
+
/** @author ddj 2026年10月22号 @param event Pointer event. @description Ignore another pointer's movement. */
|
|
89
|
+
function onMove(event) { if (event.pointerId === pointerId) move(event) }
|
|
90
|
+
/** @author ddj 2026年10月22号 @param event Pointer completion. @description Only the owning pointer ends the drag. */
|
|
91
|
+
function onEnd(event) { if (event.pointerId === pointerId) cleanup() }
|
|
92
|
+
/** @author ddj 2026年10月22号 @description Remove all drag listeners and release ownership. */
|
|
93
|
+
function cleanup() {
|
|
94
|
+
window.removeEventListener('pointermove', onMove)
|
|
95
|
+
window.removeEventListener('pointerup', onEnd)
|
|
96
|
+
window.removeEventListener('pointercancel', onEnd)
|
|
97
|
+
window.removeEventListener('blur', cleanup)
|
|
98
|
+
if (dragCleanup.current === cleanup) {
|
|
99
|
+
dragCleanup.current = null
|
|
100
|
+
seamOpening.current = false
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
dragCleanup.current = cleanup
|
|
104
|
+
window.addEventListener('pointermove', onMove)
|
|
105
|
+
window.addEventListener('pointerup', onEnd)
|
|
106
|
+
window.addEventListener('pointercancel', onEnd)
|
|
107
|
+
window.addEventListener('blur', cleanup)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @private @author ddj 2026年10月22号 @param e Starting pointer. @description Resize the visible panel until completion or collapse. */
|
|
67
111
|
const onPointerDown = (e) => {
|
|
68
112
|
e.preventDefault()
|
|
69
113
|
const startX = e.clientX
|
|
70
114
|
const startW = width
|
|
71
|
-
|
|
72
|
-
window.removeEventListener('pointermove', onMove)
|
|
73
|
-
window.removeEventListener('pointerup', onUp)
|
|
74
|
-
}
|
|
75
|
-
const onUp = () => cleanup()
|
|
115
|
+
/** @author ddj 2026年10月22号 @param ev Owning pointer movement. @description Apply this drag's width calculation. */
|
|
76
116
|
const onMove = (ev) => {
|
|
77
117
|
const raw = startW + (ev.clientX - startX)
|
|
78
118
|
// 低于最小宽度:清理监听并直接收起面板区(不夹住)
|
|
79
119
|
if (raw < wMin) {
|
|
80
|
-
|
|
120
|
+
stopDrag()
|
|
81
121
|
onHide()
|
|
82
122
|
return
|
|
83
123
|
}
|
|
84
124
|
onWidth(Math.min(W_MAX, raw))
|
|
85
125
|
}
|
|
86
|
-
|
|
87
|
-
window.addEventListener('pointerup', onUp)
|
|
126
|
+
bindDrag(e.pointerId, onMove)
|
|
88
127
|
|
|
89
128
|
}
|
|
90
129
|
|
|
@@ -98,29 +137,25 @@ export function SidebarView(props) {
|
|
|
98
137
|
const baseW = Math.max(wMin, Math.min(W_MAX, Math.round(Number(width) || wMin)))
|
|
99
138
|
let expanded = false
|
|
100
139
|
let expandStartX = 0
|
|
101
|
-
|
|
102
|
-
window.removeEventListener('pointermove', onMove)
|
|
103
|
-
window.removeEventListener('pointerup', onUp)
|
|
104
|
-
}
|
|
105
|
-
const onUp = () => cleanup()
|
|
140
|
+
/** @author ddj 2026年10月22号 @param ev Owning pointer movement. @description Apply this drag's width calculation. */
|
|
106
141
|
const onMove = (ev) => {
|
|
107
142
|
if (!expanded) {
|
|
108
143
|
if (ev.clientX - startX < PULL_OUT_THRESHOLD) return
|
|
109
144
|
expanded = true
|
|
110
145
|
expandStartX = ev.clientX
|
|
146
|
+
seamOpening.current = true
|
|
111
147
|
onShow()
|
|
112
148
|
return
|
|
113
149
|
}
|
|
114
150
|
onWidth(Math.max(wMin, Math.min(W_MAX, baseW + (ev.clientX - expandStartX))))
|
|
115
151
|
}
|
|
116
|
-
|
|
117
|
-
window.addEventListener('pointerup', onUp)
|
|
152
|
+
bindDrag(e.pointerId, onMove)
|
|
118
153
|
}
|
|
119
154
|
|
|
120
155
|
const rail = React.createElement('div', { className: 'edrv-rail' },
|
|
121
156
|
React.createElement('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', gap: 2 } },
|
|
122
157
|
panels.map((p) => {
|
|
123
|
-
const count =
|
|
158
|
+
const count = checked.find((entry) => entry.panel === p)?.badge
|
|
124
159
|
return React.createElement('button', {
|
|
125
160
|
key: p.id,
|
|
126
161
|
className: 'edrv-rail-btn' + (visible && active && p.id === active.id ? ' edrv-rail-on' : ''),
|
|
@@ -131,7 +166,8 @@ export function SidebarView(props) {
|
|
|
131
166
|
if (!visible && typeof onShow === 'function') onShow()
|
|
132
167
|
},
|
|
133
168
|
},
|
|
134
|
-
React.createElement('span', { className: 'edrv-rail-icon' },
|
|
169
|
+
React.createElement('span', { className: 'edrv-rail-icon' },
|
|
170
|
+
React.createElement(PanelBoundary, { panel: p, scope: ctx?.scope, sessionId: ctx?.sessionId, compact: true }, railIconEl(p.icon))),
|
|
135
171
|
count > 0
|
|
136
172
|
? React.createElement('span', { className: 'edrv-rail-badge' }, String(count))
|
|
137
173
|
: null)
|
|
@@ -150,7 +186,8 @@ export function SidebarView(props) {
|
|
|
150
186
|
rail,
|
|
151
187
|
visible
|
|
152
188
|
? React.createElement('div', { className: 'edrv-side-body' },
|
|
153
|
-
active ?
|
|
189
|
+
active ? React.createElement(PanelBoundary, { panel: active, scope: ctx?.scope, sessionId: ctx?.sessionId },
|
|
190
|
+
React.createElement(PanelContent, { panel: active, ctx })) : null)
|
|
154
191
|
: null,
|
|
155
192
|
visible
|
|
156
193
|
? React.createElement('div', { className: 'edrv-side-resize', onPointerDown: onPointerDown })
|