Wave Terminal 新视图(View)开发指南:从 ViewModel 到 BlockRegistry 的完整实现路径
发布时间:2026/9/13 5:48:55 作者:尧图编辑部 阅读量:1,286
开发指南:从 ViewModel 到 BlockRegistry 的完整实现路径)
Wave Terminal 新视图View开发指南从 ViewModel 到 BlockRegistry 的完整实现路径【免费下载链接】wavetermAn open-source, AI-integrated, cross-platform terminal for seamless workflows项目地址: https://gitcode.com/GitHub_Trending/wa/waveterm本指南以 Wave Terminal 仓库内的.kilocode/skills/create-view/SKILL.md为骨架完整讲解如何为 Wave Terminal 实现一种全新的视图类型View。视图是终端界面中 Block块内承载的核心内容组件——从终端、Web 预览到 CPU 监控、AI 对话面板都是不同视图类型的实例。读完本文你将掌握 Model-View 架构中ViewModel接口的全部字段语义、Jotai Atom 状态模型的组织方式、视图注册流程以及通过 CLI/RPC 创建对应 Block 的完整链路并能在frontend/app/view/下动手实现自己的视图。视图体系架构Model-View 设计总览Wave Terminal 的前端视图体系采用Model-View 架构由三个核心角色组成ViewModel承载视图全部状态、业务逻辑与 UI 配置所有状态以 Jotai atom 形式暴露。例如终端视图的TermViewModel位于 frontend/app/view/term/term-model.tsWeb 视图的WebViewModel位于 frontend/app/view/webview/webview.tsx。ViewComponent纯 React 组件只负责消费 model 中的 atoms 渲染 UI不持有业务状态。BlockFrame包裹视图的外层框架提供块头部header、连接connection管理与标准控制按钮其渲染逻辑集中在 frontend/app/block/blockframe-header.tsx。这种「模型与组件分离」的设计带来三个关键收益模型可以在不依赖 React hooks 的情况下更新状态——任何非组件代码事件回调、RPC 响应、定时器都能直接操作 atom组件保持纯粹、可测试——给定 atoms 值即可稳定渲染状态集中存放于 Jotai atoms——可被globalStore.get()/set()在任意位置访问也可被派生 atom 组合复用。ViewModel 接口全解每一个视图都必须实现ViewModel接口其完整定义位于 frontend/types/custom.d.ts全局类型声明源码中同时包含useTermHeader、hideViewName、termDurableStatus等扩展字段。核心接口如下interface ViewModel { // Required: The type identifier for this view (e.g., term, web, preview) viewType: string; // Required: The React component that renders this view viewComponent: ViewComponentViewModel; // Optional: Icon shown in block header (FontAwesome icon name or IconButtonDecl) viewIcon?: jotai.Atomstring | IconButtonDecl; // Optional: Display name shown in block header (e.g., Terminal, Web, Preview) viewName?: jotai.Atomstring; // Optional: Additional header elements (text, buttons, inputs) shown after the name viewText?: jotai.Atomstring | HeaderElem[]; // Optional: Icon button shown before the view name in header preIconButton?: jotai.AtomIconButtonDecl; // Optional: Icon buttons shown at the end of the header (before settings/close) endIconButtons?: jotai.AtomIconButtonDecl[]; // Optional: Custom background styling for the block blockBg?: jotai.AtomMetaType; // Optional: If true, completely hides the block header noHeader?: jotai.Atomboolean; // Optional: If true, shows connection picker in header for remote connections manageConnection?: jotai.Atomboolean; // Optional: If true, filters out nowsh connections from connection picker filterOutNowsh?: jotai.Atomboolean; // Optional: If true, removes default padding from content area noPadding?: jotai.Atomboolean; // Optional: Atoms for managing in-block search functionality searchAtoms?: SearchAtoms; // Optional: Returns whether this is a basic terminal (for multi-input feature) isBasicTerm?: (getFn: jotai.Getter) boolean; // Optional: Returns context menu items for the settings dropdown getSettingsMenuItems?: () ContextMenuItem[]; // Optional: Focuses the view when called, returns true if successful giveFocus?: () boolean; // Optional: Handles keyboard events, returns true if handled keyDownHandler?: (e: WaveKeyboardEvent) boolean; // Optional: Cleanup when block is closed dispose?: () void; }关键概念Atoms 与 ViewComponentAtoms所有 UI 相关属性必须是 Jotai atom这带来了三项能力状态变化时的响应式更新组件通过useAtomValue订阅可从任何位置通过globalStore.get()/globalStore.set()命令式访问支持派生 atomderived atoms根据其它 atom 计算新值例如根据 block 元数据动态生成头部按钮。ViewComponentReact 组件接收以下 props见 frontend/types/custom.d.ts 中的ViewComponentProps定义type ViewComponentPropsT extends ViewModel { blockId: string; // Unique ID for this block blockRef: React.RefObjectHTMLDivElement; // Ref to block container contentRef: React.RefObjectHTMLDivElement; // Ref to content area model: T; // Your ViewModel instance };四步实现一个新视图步骤 1创建 View Model 类在视图目录例如frontend/app/view/下新建myview/子目录创建视图模型文件实现ViewModelimport { BlockNodeModel } from /app/block/blocktypes; import { globalStore } from /app/store/jotaiStore; import { WOS, useBlockAtom } from /store/global; import * as jotai from jotai; import { MyView } from ./myview; export class MyViewModel implements ViewModel { viewType: string; blockId: string; nodeModel: BlockNodeModel; blockAtom: jotai.AtomBlock; // Define your atoms (simple field initializers) viewIcon jotai.atomstring(circle); viewName jotai.atomstring(My View); noPadding jotai.atomboolean(true); // Derived atom (created in constructor) viewText!: jotai.AtomHeaderElem[]; constructor(blockId: string, nodeModel: BlockNodeModel) { this.viewType myview; this.blockId blockId; this.nodeModel nodeModel; this.blockAtom WOS.getWaveObjectAtomBlock(block:${blockId}); // Create derived atoms that depend on block data or other atoms this.viewText jotai.atom((get) { const blockData get(this.blockAtom); const rtn: HeaderElem[] []; // Add header buttons/text based on state rtn.push({ elemtype: iconbutton, icon: refresh, title: Refresh, click: () this.refresh(), }); return rtn; }); } get viewComponent(): ViewComponent { return MyView; } refresh() { // Update state using globalStore // Never use React hooks in model methods console.log(refreshing...); } giveFocus(): boolean { // Focus your view component return true; } dispose() { // Cleanup resources (unsubscribe from events, etc.) } }关键点blockAtom通过WOS.getWaveObjectAtomBlock(\block:${blockId})绑定到该 block 的 WOSWave Object Store对象之后即可在派生 atom 中get(this.blockAtom)读取 block 数据如meta 字段实现「头部 UI 随 block 状态联动」。步骤 2创建 View 组件创建对应的 React 组件文件如myview.tsximport { ViewComponentProps } from /app/block/blocktypes; import { MyViewModel } from ./myview-model; import { useAtomValue } from jotai; import ./myview.scss; export const MyView: React.FCViewComponentPropsMyViewModel ({ blockId, model, contentRef }) { // Use atoms from the model (these are React hooks - call at top level!) const blockData useAtomValue(model.blockAtom); return ( div classNamemyview-container ref{contentRef} divBlock ID: {blockId}/div divView: {model.viewType}/div {/* Your view content here */} /div ); };注意contentRef挂载到内容容器上blockId与model分别用于标识与状态读取。步骤 3注册视图到 BlockRegistry视图类型必须注册进 frontend/app/block/blockregistry.ts 中的BlockRegistryimport { MyViewModel } from /app/view/myview/myview-model; const BlockRegistry: Mapstring, ViewModelClass new Map(); BlockRegistry.set(term, TermViewModel); BlockRegistry.set(preview, PreviewModel); BlockRegistry.set(web, WebViewModel); // ... existing registrations ... BlockRegistry.set(myview, MyViewModel); // Add your view here注册表键如myview即成为 block 元数据中的视图类型。从仓库现状看已注册的视图类型包括term、preview、web、waveai、cpuplot、sysinfo、vdom、tips、help、launcher、tsunami、aifilediff、waveconfig、processviewer可作参考。makeViewModel()在 blockregistry.ts 中负责按blockView查找构造函数实例化若未注册则回退到makeDefaultViewModel()用 blockutil.tsx 中的blockViewToIcon/blockViewToName提供默认图标与名称。步骤 4创建对应视图类型的 Block用户可以通过以下方式创建使用你视图类型的 blockCLI 创建在 shell 内执行wsh createblock myview keyvalue ...其中第一个参数即视图名viewname。该命令的实现在 cmd/wsh/cmd/wshcmd-createblock.go它解析keyvalue形式的元数据参数后设置meta[view] viewName再通过wshclient.CreateBlockCommand发起 RPC支持-m/--magnified以放大模式创建。元数据方式将 block 的meta.view字段设置为myview——BlockFrame 渲染时会读取该字段并经makeViewModel分派到对应视图模型。RPC 方式直接调用CreateBlockCommand在BlockDef.Meta中携带view键与 createblock 命令的底层路径一致。补充说明仓库中另有wsh view {file|directory|URL}命令别名preview/open用于预览或编辑文件/目录并将meta.view设为preview实现在 cmd/wsh/cmd/wshcmd-view.go如果你的视图定位是内容预览类也可以参考该命令的构造方式。真实案例剖析案例 1终端视图term-model.tsTermViewModel位于 frontend/app/view/term/term-model.ts是仓库中最复杂的视图模型展示了几乎所有高级能力连接管理通过manageConnectionatom 控制是否在头部显示连接选择器——当处于 vdom 模式时隐藏普通终端模式显示this.manageConnection jotai.atom((get) { const termMode get(this.termMode); if (termMode vdom) return false; return true; // Show connection picker for regular terminal mode });动态头部按钮根据 shell 进程状态shellProcStatus渲染重启按钮this.endIconButtons jotai.atom((get) { const shellProcStatus get(this.shellProcStatus); const buttons: IconButtonDecl[] []; if (shellProcStatus running) { buttons.push({ elemtype: iconbutton, icon: refresh, title: Restart Shell, click: this.forceRestartController.bind(this), }); } return buttons; });模式切换在 terminal 与 vdom 视图间切换termMode派生自blockData.meta[term:mode]头部图标与名称随之变化自定义键盘处理keyDownHandler处理终端专属快捷键焦点管理giveFocus()聚焦 xterm.js 实例Shell 集成状态通过termDurableStatus等 atom 呈现 AI 能力指示如命令退出码成功/失败图标见 term-model.ts 中基于shellprocexitcode渲染check或xmark-large的逻辑。案例 2Web 视图webview.tsxWebViewModel位于 frontend/app/view/webview/webview.tsx展示了复杂头部控件的组织方式复杂头部控件后退/前进/主页/URL 输入框状态管理加载状态、URL、导航状态事件处理webview 导航事件自定义样式noPadding实现全幅内容媒体控制媒体激活时显示播放/暂停/静音按钮。其viewText派生 atom 是头部元素编排的范本this.viewText jotai.atom((get) { const url get(this.url); const rtn: HeaderElem[] []; // Navigation buttons rtn.push({ elemtype: iconbutton, icon: chevron-left, click: this.handleBack.bind(this), disabled: this.shouldDisableBackButton(), }); // URL input with nested controls rtn.push({ elemtype: div, className: block-frame-div-url, children: [ { elemtype: input, value: url, onChange: this.handleUrlChange.bind(this), onKeyDown: this.handleKeyDown.bind(this), }, { elemtype: iconbutton, icon: rotate-right, click: this.handleRefresh.bind(this), }, ], }); return rtn; });头部元素体系HeaderElemviewTextatom 可以返回以下元素类型的数组完整类型定义见 frontend/types/custom.d.ts实际为IconButtonDecl | ToggleIconButtonDecl | HeaderText | HeaderInput | HeaderDiv | HeaderTextButton | ConnectionButton | MenuButton的联合类型图标按钮iconbutton——最常见的头部操作元素{ elemtype: iconbutton, icon: refresh, title: Tooltip text, click: () { /* handler */ }, disabled?: boolean, iconColor?: string, iconSpin?: boolean, noAction?: boolean, // Shows icon but no click action }文本元素text{ elemtype: text, text: Display text, className?: string, noGrow?: boolean, ref?: React.RefObjectHTMLElement, onClick?: (e: React.MouseEvent) void, }文本按钮textbutton{ elemtype: textbutton, text: Button text, className?: string, title: Tooltip, onClick: (e: React.MouseEvent) void, }输入框input{ elemtype: input, value: string, className?: string, onChange: (e: React.ChangeEventHTMLInputElement) void, onKeyDown?: (e: React.KeyboardEventHTMLInputElement) void, onFocus?: (e: React.FocusEventHTMLInputElement) void, onBlur?: (e: React.FocusEventHTMLInputElement) void, ref?: React.RefObjectHTMLInputElement, }容器div——可嵌套子元素组合复杂布局{ elemtype: div, className?: string, children: HeaderElem[], onMouseOver?: (e: React.MouseEvent) void, onMouseOut?: (e: React.MouseEvent) void, }菜单按钮menubutton——下拉菜单{ elemtype: menubutton, // ... MenuButtonProps ... }另有toggleiconbutton绑定activeatom 的开关按钮与connectionbutton连接状态按钮两种类型。blockframe-header.tsx正是按这些声明渲染头部先渲染preIconButton再渲染viewIcon与viewName接着是viewText元素序列最后追加endIconButtons当manageConnection为真时还会插入连接按钮。最佳实践Jotai Model 模式遵循以下四条规则简单 atom 用字段初始化器viewIcon jotai.atomstring(circle); noPadding jotai.atomboolean(true);需要依赖其它 atom 的派生 atom 在构造函数中创建constructor(blockId: string, nodeModel: BlockNodeModel) { this.viewText jotai.atom((get) { const blockData get(this.blockAtom); return [/* computed based on blockData */]; }); }模型方法绝不使用 React hooks——改用globalStore.get()/set()refresh() { const currentData globalStore.get(this.blockAtom); globalStore.set(this.dataAtom, newData); }组件中使用 hooks 订阅 atomconst data useAtomValue(model.dataAtom); const [value, setValue] useAtom(model.valueAtom);状态管理所有视图状态都应存放在模型上的 atom 中用useBlockAtom()辅助函数创建与 block 生命周期绑定且持久的 atom——它在 frontend/app/store/global.ts 中实现基于按 blockId 维度的缓存blockCache同一 block 下同名的 atom 只会创建一次在 React 组件之外需要命令式访问时使用globalStore使用waveEventSubscribe()订阅 Wave 事件如进程状态、连接状态更新事件驱动模型更新。样式为视图创建独立的.scss文件尽量使用 Tailwind 工具类v4全幅内容full-bleed时设置noPadding: atom(true)用blockBgatom 自定义 block 背景。焦点管理实现giveFocus()以在以下时机聚焦视图Block 通过键盘导航获得焦点时用户点击 block 时成功聚焦返回true否则返回false。键盘处理实现keyDownHandler(e: WaveKeyboardEvent)处理视图专属快捷键事件被处理后返回true阻止继续传播使用keyutil.checkKeyPressed(waveEvent, Cmd:K)这类工具判断快捷键见 frontend/util/keyutil.ts。清理dispose实现dispose()完成资源释放退订 Wave 事件如term-model.ts中保存shellProcStatusUnsubFn等退订函数并在dispose中调用注销路由/处理器清除定时器/间隔释放其它资源。连接管理需要远程连接的视图this.manageConnection jotai.atom(true); // Show connection picker this.filterOutNowsh jotai.atom(true); // Hide nowsh connections访问连接状态const connStatus jotai.atom((get) { const blockData get(this.blockAtom); const connName blockData?.meta?.connection; return get(getConnStatusAtom(connName)); });getConnStatusAtom在 frontend/app/store/global.ts 中定义返回对应连接的PrimitiveAtomConnStatus连接状态变更会通过事件总线推送到该 atom。常见模式读取 Block 元数据视图可以通过getBlockMetaKeyAtom读取 block 的 meta 键值如自定义的myview:flagimport { getBlockMetaKeyAtom } from /store/global; // In constructor: this.someFlag getBlockMetaKeyAtom(blockId, myview:flag); // In component: const flag useAtomValue(model.someFlag);该函数在 frontend/app/store/global.ts 中实现从 WOS 的 block 对象中读取blockData?.meta?.[key]并缓存到 per-block atom 缓存中保证同键名 atom 唯一。配置覆盖Override ConfigWave 采用全局 → 连接 → block的层级配置系统。getOverrideConfigAtom(blockId, key)按「block meta → connection config → global settings」的顺序解析取值import { getOverrideConfigAtom } from /store/global; this.settingAtom jotai.atom((get) { // Checks block meta, then connection config, then global settings return get(getOverrideConfigAtom(this.blockId, myview:setting)) ?? defaultValue; });查看 frontend/app/store/global.ts 中getOverrideConfigAtom的实现可见其解析顺序先查getBlockMetaKeyAtom(blockId, key)block 级 meta再查getConnConfigKeyAtom(connName, key)连接级配置由 block 的meta.connection定位最后落到getSettingsKeyAtom(key)全局设置。这意味着你的视图可以天然支持「按 block 覆盖、按连接覆盖、全局兜底」的配置行为。更新 Block 元数据通过 RPC 写入元数据例如持久化视图内部状态import { RpcApi } from /app/store/wshclientapi; import { TabRpcClient } from /app/store/wshrpcutil; import { WOS } from /store/global; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef(block, this.blockId), meta: { myview:key: value }, });结语与延伸阅读实现一个新视图的完整路径可以概括为编写实现ViewModel的模型类atoms 承载状态→ 编写消费 atoms 的 React 组件 → 在BlockRegistry注册视图类型 → 通过wsh createblock/meta.view/ RPC 创建对应 Block。模型与组件分离、状态全部 atom 化、头部 UI 声明式编排是 Wave Terminal 视图体系的核心设计哲学。建议继续深入阅读以下文件frontend/app/block/blockframe-header.tsx —— Block 头部渲染逻辑header 如何消费viewText/viewIcon/manageConnection等frontend/app/view/term/term-model.ts —— 最复杂的视图模型参考frontend/app/view/webview/webview.tsx —— 导航 UI 与复杂头部控件参考frontend/types/custom.d.ts —— 全部类型定义ViewModel、HeaderElem、ViewComponentProps、SearchAtomsfrontend/app/block/blockregistry.ts —— 视图注册表frontend/app/store/global.ts —— atom 工具函数与层级配置解析【免费下载链接】wavetermAn open-source, AI-integrated, cross-platform terminal for seamless workflows项目地址: https://gitcode.com/GitHub_Trending/wa/waveterm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考