Netcatty 复制多窗格 Workspace 标签页:布局树克隆与逐窗格 cwd 继承的设计与实现 【免费下载链接】NetcattySSH workspace, SFTP, and terminals in one项目地址https://gitcode.com/gh_mirrors/net/Netcatty点击查看免费下载导读NetcattySSH workspace、SFTP 与终端一体化原有的Copy Tab只能复制单会话标签页遇到承载多个 split/focus 窗格的 workspace 标签页就会静默失效。本文基于仓库内的设计文档 2026-07-29-copy-workspace-split-tab-design.md 与配套实现计划 2026-07-29-copy-workspace-split-tab.md完整讲解如何把单会话复制能力上移一层到 workspace 层级通过新增纯函数cloneWorkspaceTree深拷贝布局树并重映射 sessionId逐窗格克隆会话并继承各自的工作目录最终以复制标签页 双击复制两个入口落地。读完你将掌握 Netcatty 中 workspace 布局树模型、StrictMode 安全的 ID 预铸、SSH/proccwd 探测与cd --注入等核心机制。背景Workspace 标签页模型与现有的单会话复制能力Workspace 布局树模型Netcatty 中一个承载多个窗格的标签页称为 workspace 标签页其数据模型定义在 domain/models/workspace.tsWorkspaceNode是一棵递归布局树pane{sessionId}叶子节点挂载一个真实会话split{direction, children, sizes?}内部节点描述水平/垂直分屏比例Workspace除root外还携带viewModesplit平铺 /focus左侧列表 单终端、可选的focusedSessionId、focusSessionOrderfocus 模式侧边栏的会话顺序、snippetId由运行 snippet 创建时记录与title关键点workspace 标签页的 tab id 就是 workspace idws-…而不是某个 session id。这一差异正是后续复制逻辑需要单独处理的核心。配套的纯函数树操作集中在 domain/workspace.tscollectSessionIdsL312-L317、insertPaneIntoWorkspaceL106、pruneWorkspaceNodeL13-L53、updateWorkspaceSplitSizesL163等。设计文档明确指出此前没有任何一个助手能深克隆一棵树的同时重映射其 sessionId——这正是本次要补齐的唯一缺口。单会话复制链路copySession单标签页复制已经是一条成熟的链路状态动作application/state/useSessionState.ts 中的copySession(sessionId, { localShellType, inheritedCwd, reuseConnection })在setSessions更新器之外预先铸出新 session idStrictMode 双调用安全在更新器内通过createCopiedTerminalSessionClone克隆会话再用insertCopiedTabOrderIdOnceL118把新 tab id 插入源标签页右侧。克隆工厂application/state/terminalConnectionReuse.tscreateTerminalSessionClone复制 host/连接身份hostId/hostLabel/hostname/username/port、pluginConnection 等对本地会话把localStartDir设为inheritedCwd对 SSH 会话设置pendingInitialCwd inheritedCwd驱动首次连接时注入cd -- quotedstatus: connecting源为已连接 SSH 时设置reuseConnectionFromSessionId以便复用既有 SSH 通道。cwd 捕获application/app/AppHandlers.ts 的captureCtxInheritedCwd(getCtx, sessionId)按优先级解析会话当前目录实时 OSC 7 值getSessionRestoreCwd→ SSH/proc探测getSessionPwd→localStartDir并带有网络设备豁免与shouldProbeSessionCwd门控。App 处理器copySessionWithCurrentShellImplL571-L590先捕获 cwd再调用copySession并依据reuseConnection决定是否继承 cwd全新登录可能停在堡垒机选机界面此时不应向新登录发送旧目录命令本地重复仍继承目录。UI 入口会话标签页通过components/top-tabs/TopTabItems.tsx的右键菜单Copy Tab和双击触发。现状缺口copySession对 workspace id 会静默 no-opprevSessions.find(id)找不到id 是 workspace id 而非 session id且它只克隆一个会话对布局树毫无概念。为什么今天复制多窗格标签页会失败设计文档把根因归结为两个相互独立的缺口没有入口——WorkspaceTopTabcomponents/top-tabs/TopTabItems.tsx的右键菜单只有 Rename / Detach / Close没有Copy Tab项也没有双击复制处理器没有处理器——即便强行调用copySession(workspaceId)也会在 sessions 中查不到而原样返回它克隆恰好一个会话对布局树一无所知。因此本次改造必须同时补上入口UI与处理器domain state App 三层把单会话复制路径整体上移一层到 workspace 层级。设计决策来自 brainstorming入口对齐与单会话标签页一致——workspace 标签页右键菜单增加Copy Tab并启用双击复制覆盖范围复制所有workspace 标签页split平铺与focus列表 单终端两种视图模式都保留视图模式与布局cwd 继承每个窗格独立执行与单标签页完全一致的行为实时 cwd → SSH 探测 →localStartDir回退明确不做YAGNI复制产生的都是全新会话与单标签页复制同一工厂createCopiedTerminalSessionClone不覆盖连接复用语义不做窗格内容实时复制不做广播模式的复制新 workspace id 天然不在broadcastWorkspaceIds中复制品自动以非广播状态启动。架构分层落地分层遵循 AGENTS.md纯树逻辑放domain/编排放useSessionStatehook异步 cwd 捕获放 App 处理器表现层仅做接线。第 1 层纯领域助手cloneWorkspaceTree核心新函数定义在 domain/workspace.ts/** * Deep-clone a workspace layout tree, minting fresh node ids and remapping * every panes sessionId via sessionIdMap (old id - new id). Split * direction and sizes are preserved. A sessionId absent from the map is * kept as-is. Pure; does not mutate node. */ export const cloneWorkspaceTree ( node: WorkspaceNode, sessionIdMap: ReadonlyMapstring, string, nodeIdMap?: ReadonlyMapstring, string, ): WorkspaceNode { if (node.type pane) { return { id: nodeIdMap?.get(node.id) ?? crypto.randomUUID(), type: pane, sessionId: sessionIdMap.get(node.sessionId) ?? node.sessionId, }; } return { id: nodeIdMap?.get(node.id) ?? crypto.randomUUID(), type: split, direction: node.direction, children: node.children.map(child cloneWorkspaceTree(child, sessionIdMap, nodeIdMap)), ...(node.sizes ? { sizes: [...node.sizes] } : {}), }; };要点pane→ 铸新节点 idsessionId sessionIdMap.get(old) ?? old映射缺失时原样保留见下单窗格 workspace边界split→ 铸新节点 id保留directionsizes用展开拷贝[...node.sizes]避免引用共享children 递归克隆不引入任何 React/Electron 依赖纯函数、可单测、不修改源树与设计稿相比落地实现增加了可选参数nodeIdMap由上层copyWorkspace预先为所有树节点铸好新 id见下进一步保证 StrictMode 双调用下连布局树节点 id 都不会重复。第 2 层状态层buildCopiedWorkspace纯助手 copyWorkspace动作纯助手application/state/useSessionState.tsbuildCopiedWorkspace(sourceWorkspace, prevSessions, { newWorkspaceId, sessionIdMap, localShellType?, perPaneCwd?, nodeIdMap? })用collectSessionIds收集源布局树的全部 session id过滤出在prevSessions中仍然存活的liveIds若一个都不剩返回null调用方整体 no-op对已关闭的源会话反复调用pruneWorkspaceNode(prunedRoot, deadId)从克隆树中剪除其窗格保证克隆树绝不引用不存在的会话基于liveIdMap逐会话调用createCopiedTerminalSessionClone(src, { id, workspaceId: newWorkspaceId, localShellType, inheritedCwd: perPaneCwd?.[srcId] })组装新Workspaceroot cloneWorkspaceTree(prunedRoot, liveIdMap, nodeIdMap)保留viewMode/title/snippetIdfocusedSessionId经liveIdMap重映射源焦点窗格已亡则回退到第一个存活新 idfocusSessionOrder逐项重映射并过滤掉已死 id。状态动作copyWorkspace(workspaceId, { localShellType?, perPaneCwd? })L1206-L1260与copySession并排在setSessions更新器之外预铸newWorkspaceId ws- crypto.randomUUID()、sessionIdMap旧→新以及nodeIdMap旧树节点 id → 新树节点 id更新器内用workspacesRef.current做防复活检查cwd 捕获是异步的避免在探测期间 workspace 已被关闭/结构变更后把它复活并用prevSessions.some(session newSessionIds.has(session.id))防止重复插入嵌套的addWorkspaceIfMissingL72、setActiveTabId(newWorkspaceId)、insertCopiedTabOrderIdOnce(prevTabOrder, workspaceId, newWorkspaceId, allTabIds)均为幂等原语StrictMode 双调用无害返回[...prevSessions, ...built.newSessions]新会话与 workspace 一起注册新标签页成为活动标签页。第 3 层App 处理器copyWorkspaceWithCurrentShellImplapplication/app/AppHandlers.tsexport async function copyWorkspaceWithCurrentShellImpl(getCtx: AppContextGetter, workspaceId: string) { const { classifyLocalShellType, collectSessionIds, copyWorkspace, discoveredShells, resolveShellSetting, terminalSettings, workspaces } getCtx(); const workspace workspaces.find((w: { id: string }) w.id workspaceId); if (!workspace) return; const sessionIds: string[] collectSessionIds(workspace.root); // Resolve each panes cwd in parallel — SSH panes may await the /proc probe. const entries await Promise.all( sessionIds.map(async (id): Promisereadonly [string, string | undefined] [id, await captureCtxInheritedCwd(getCtx, id)] as const), ); const perPaneCwd: Recordstring, string | undefined Object.fromEntries(entries); const resolved resolveShellSetting(terminalSettings.localShell, discoveredShells); const userAgent typeof navigator ! undefined ? navigator.userAgent : ; return copyWorkspace(workspaceId, { localShellType: classifyLocalShellType(resolved?.command || terminalSettings.localShell, userAgent), perPaneCwd, }); }设计要点workspace 不存在直接返回对应点击后 workspace 被关闭的边界对每个窗格并行Promise.all调用captureCtxInheritedCwd——SSH 窗格可能等待/proc探测串行会放大延迟captureCtxInheritedCwd是本文件内模块私有函数同文件直接复用classifyLocalShellType用于把配置的本地 shell 解析为本地克隆所需的 shell 类型。第 4 层UI 接线表现层从当前仓库源码看设计已完整落地在 components/top-tabs/TopTabItems.tsxWorkspaceTopTabProps新增onCopyWorkspace: (workspaceId: string) voidL931组件内用createTopTabCopyDoubleClickHandler(onCopyWorkspace, workspace.id)L996复用会话标签页的双击工厂绑定onDoubleClick右键菜单在 Rename 之后插入ContextMenuItem onClick{() onCopyWorkspace(workspace.id)}文案复用 i18n 键tabs.copyTabL1091无需新增翻译键——计划文档注明该键位于application/i18n/locales/en/terminal.ts。其余接线计划文档 Task 5/6App.tsx从useSessionState解构copyWorkspace并绑定copyWorkspaceWithCurrentShellImpl为copyWorkspaceWithCurrentShell回调经AppView的 ctx 传入TopTabs的onCopyWorkspaceproptopTabsAreEqual备忘录比较中加入onCopyWorkspace以维持重渲染正确性。完整数据流设计文档给出的端到端链路与源码实现一致右键 Copy Tab / 双击 workspace 标签页 - onCopyWorkspace(workspaceId) copyWorkspaceWithCurrentShell (async) - 对每个会话并行 captureCtxInheritedCwd (实时cwd | ssh /proc | localStartDir) - copyWorkspace(workspaceId, { perPaneCwd, localShellType }) - 预铸 newWsId sessionIdMap(old-new) nodeIdMap - buildCopiedWorkspace 过滤存活会话 - 修剪死窗格 - 逐会话克隆(新workspaceId, inheritedCwdperPaneCwd[old]) - root cloneWorkspaceTree(prunedRoot, liveIdMap, nodeIdMap) - focus 字段重映射、viewMode/title/snippetId 保留 - 注册新会话 新 workspacetabOrder 插入源右侧setActiveTabId(newWsId) - 每个新窗格开始连接本地用 localStartDirSSH 首次连接注入 cd -- cwd克隆工厂对 cwd 的应用逻辑terminalConnectionReuse.ts值得单独强调injectsInheritedCwd仅对ssh/undefined协议且非 mosh/et 的会话成立——为 telnet/serial/mosh/et 设置pendingInitialCwd会是永不消费的死状态本地会话则直接把localStartDir换成继承目录原生 pty spawn cwd对 powershell/cmd 同样适用。边界情况与错误处理设计文档与实现共同覆盖的边界场景行为点击后 workspace 已关闭copyWorkspace查不到源即 no-op异步探测期间被关闭则由workspacesRef防复活检查拦截某个窗格的会话在操作中途关闭跳过该会话用pruneWorkspaceNode从克隆树剪除其窗格树绝不引用缺失会话全部窗格会话都已关闭buildCopiedWorkspace返回null整体 no-op单窗格 workspaceroot 是裸panecloneWorkspaceTree泛化处理天然支持远程窗格没有已知 cwdinheritedCwd为 undefined → 与今天行为一致SSH 登录目录同单标签页复制StrictMode 双调用session id、workspace id、树节点 id 全部在更新器外预铸嵌套setActiveTabId/setTabOrder幂等广播模式新 workspace id 不在broadcastWorkspaceIds中 → 复制品自动非广播无需额外处理SSH 探测失败/超时视为未知 cwd不向用户报错复制照常打开网络设备如华为 VRP 类captureCtxInheritedCwd中isNetworkDevice豁免不发起额外 exec 通道探测测试策略设计文档与计划文档给出的测试方案运行方式node --test --import tsx filedomain/workspace.test.ts——cloneWorkspaceTree经映射重映射所有pane.sessionId铸全新节点 id与源不共享嵌套树上保留direction与sizes映射为空时保持原 sessionIdapplication/state/copyWorkspace.test.ts计划文档新建——buildCopiedWorkspace克隆 N 个新会话 1 个 workspace克隆树只引用新 session id源 id 不泄漏保留viewMode/重映射后的 focus 字段源会话已关闭的窗格被剪除、焦点回退无存活会话时返回nulluseSessionStateWorkspaceStrictMode.test.ts—— 幂等原语addWorkspaceIfMissing、insertCopiedTabOrderIdOnce的既有覆盖application/app/AppHandlers.test.ts——copyWorkspaceWithCurrentShellImpl为每个窗格解析inheritedCwd本地 →localStartDirSSH → 探测组装进perPaneCwdworkspace 消失时 no-op手动验证计划文档 Task 7本地终端 split 2–3 个窗格并各自cd到不同目录 → 右键/双击复制 → 新标签页保持相同 split 布局且每个新窗格pwd与源一致SSH 分屏重复每个 SSH 窗格连接后cd到源窗格远端目录focus 模式多会话标签页验证视图模式保留。涉及文件一览文件改动domain/workspace.ts domain/workspace.test.ts新增cloneWorkspaceTreeapplication/state/useSessionState.ts 测试新增buildCopiedWorkspace与copyWorkspace动作application/app/AppHandlers.ts AppHandlers.test.ts新增copyWorkspaceWithCurrentShellImpl复用captureCtxInheritedCwdApp.tsx、application/app/AppView.tsx、components/TopTabs.tsx、components/top-tabs/TopTabItems.tsxUI 入口 prop 线程 memo 比较i18n复用既有tabs.copyTab无新键小结复制多窗格 workspace 标签页的本质是把单会话copySession路径在 workspace 层级重演一遍新增唯一一个纯领域助手cloneWorkspaceTree解决深克隆 sessionId 重映射的空白buildCopiedWorkspace/copyWorkspace以 StrictMode 安全的 ID 预铸完成状态层编排copyWorkspaceWithCurrentShellImpl并行捕获逐窗格 cwdUI 层仅需对齐会话标签页的右键菜单 双击两个入口。这一设计贯穿了 Netcatty 的分层纪律——纯逻辑留在domain/、编排留在 state hook、异步探测留在 App 处理器、表现留在组件——也是理解其 workspace 布局树与 cwd 继承机制的最佳入口。赞分享【免费下载链接】NetcattySSH workspace, SFTP, and terminals in one项目地址https://gitcode.com/gh_mirrors/net/Netcatty点击查看免费下载相关推荐Galgame引擎百科全书开发者与爱好者的终极技术指南Galgame引擎百科全书开发者与爱好者的终极技术指南 在视觉小说和Galgame开发的世界中选择合适的引擎往往是项目成功的关键。Galgame Engin知识库文档游戏开发高效掌控ImHex视图多标签页与窗口布局全指南高效掌控ImHex视图多标签页与窗口布局全指南 你是否在使用ImHex时因文件太多而迷失在单窗口界面是否希望像浏览器标签页一样灵活管理多个二进制文件本文将桌面应用开发工具逆向工程WezTerm 的 tab:panes_with_info()在 Lua 配置中全面读取标签页内所有窗格布局信息WezTerm 的 tab:panes_with_info 在 Lua 配置中全面读取标签页内所有窗格布局信息 tab:panes_with_info 是 W桌面应用开发工具跨平台上一篇【免费下载】 CS5466VL171 TypeC母座正反插转HDMI方案原理图下一篇OpenWebRX 开源项目教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考