create-view

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Creating a New View in Wave Terminal

在Wave Terminal中创建新视图

This guide explains how to implement a new view type in Wave Terminal. Views are the core content components displayed within blocks in the terminal interface.
本指南介绍如何在Wave Terminal中实现新的视图类型。视图是终端界面中区块内显示的核心内容组件。

Architecture Overview

架构概述

Wave Terminal uses a Model-View architecture where:
  • ViewModel - Contains all state, logic, and UI configuration as Jotai atoms
  • ViewComponent - Pure React component that renders the UI using the model
  • BlockFrame - Wraps views with a header, connection management, and standard controls
The separation between model and component ensures:
  • Models can update state without React hooks
  • Components remain pure and testable
  • State is centralized in Jotai atoms for easy access
Wave Terminal采用模型-视图架构,其中:
  • ViewModel - 包含所有状态、逻辑和UI配置,以Jotai atoms形式存在
  • ViewComponent - 纯React组件,使用模型渲染UI
  • BlockFrame - 为视图添加头部、连接管理和标准控件的包装器
模型与组件的分离确保:
  • 模型可在不使用React hooks的情况下更新状态
  • 组件保持纯净且可测试
  • 状态集中存储在Jotai atoms中,便于访问

ViewModel Interface

ViewModel接口

Every view must implement the
ViewModel
interface defined in
frontend/types/custom.d.ts
:
typescript
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: ViewComponent<ViewModel>;

  // Optional: Icon shown in block header (FontAwesome icon name or IconButtonDecl)
  viewIcon?: jotai.Atom<string | IconButtonDecl>;

  // Optional: Display name shown in block header (e.g., "Terminal", "Web", "Preview")
  viewName?: jotai.Atom<string>;

  // Optional: Additional header elements (text, buttons, inputs) shown after the name
  viewText?: jotai.Atom<string | HeaderElem[]>;

  // Optional: Icon button shown before the view name in header
  preIconButton?: jotai.Atom<IconButtonDecl>;

  // Optional: Icon buttons shown at the end of the header (before settings/close)
  endIconButtons?: jotai.Atom<IconButtonDecl[]>;

  // Optional: Custom background styling for the block
  blockBg?: jotai.Atom<MetaType>;

  // Optional: If true, completely hides the block header
  noHeader?: jotai.Atom<boolean>;

  // Optional: If true, shows connection picker in header for remote connections
  manageConnection?: jotai.Atom<boolean>;

  // Optional: If true, filters out 'nowsh' connections from connection picker
  filterOutNowsh?: jotai.Atom<boolean>;

  // Optional: If true, removes default padding from content area
  noPadding?: jotai.Atom<boolean>;

  // 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;
}
每个视图都必须实现
frontend/types/custom.d.ts
中定义的
ViewModel
接口:
typescript
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: ViewComponent<ViewModel>;

  // Optional: Icon shown in block header (FontAwesome icon name or IconButtonDecl)
  viewIcon?: jotai.Atom<string | IconButtonDecl>;

  // Optional: Display name shown in block header (e.g., "Terminal", "Web", "Preview")
  viewName?: jotai.Atom<string>;

  // Optional: Additional header elements (text, buttons, inputs) shown after the name
  viewText?: jotai.Atom<string | HeaderElem[]>;

  // Optional: Icon button shown before the view name in header
  preIconButton?: jotai.Atom<IconButtonDecl>;

  // Optional: Icon buttons shown at the end of the header (before settings/close)
  endIconButtons?: jotai.Atom<IconButtonDecl[]>;

  // Optional: Custom background styling for the block
  blockBg?: jotai.Atom<MetaType>;

  // Optional: If true, completely hides the block header
  noHeader?: jotai.Atom<boolean>;

  // Optional: If true, shows connection picker in header for remote connections
  manageConnection?: jotai.Atom<boolean>;

  // Optional: If true, filters out 'nowsh' connections from connection picker
  filterOutNowsh?: jotai.Atom<boolean>;

  // Optional: If true, removes default padding from content area
  noPadding?: jotai.Atom<boolean>;

  // 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;
}

Key Concepts

核心概念

Atoms: All UI-related properties must be Jotai atoms. This enables:
  • Reactive updates when state changes
  • Access from anywhere via
    globalStore.get()
    /
    globalStore.set()
  • Derived atoms that compute values from other atoms
ViewComponent: The React component receives these props:
typescript
type ViewComponentProps<T extends ViewModel> = {
  blockId: string; // Unique ID for this block
  blockRef: React.RefObject<HTMLDivElement>; // Ref to block container
  contentRef: React.RefObject<HTMLDivElement>; // Ref to content area
  model: T; // Your ViewModel instance
};
Atoms:所有与UI相关的属性必须是Jotai atoms。这支持:
  • 状态变化时的响应式更新
  • 通过
    globalStore.get()
    /
    globalStore.set()
    从任何地方访问
  • 从其他原子计算值的派生原子
ViewComponent:React组件会接收以下props:
typescript
type ViewComponentProps<T extends ViewModel> = {
  blockId: string; // Unique ID for this block
  blockRef: React.RefObject<HTMLDivElement>; // Ref to block container
  contentRef: React.RefObject<HTMLDivElement>; // Ref to content area
  model: T; // Your ViewModel instance
};

Step-by-Step Guide

分步指南

1. Create the View Model Class

1. 创建视图模型类

Create a new file for your view model (e.g.,
frontend/app/view/myview/myview-model.ts
):
typescript
import { BlockNodeModel } from "@/app/block/blocktypes";
import { WOS, globalStore, 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.Atom<Block>;

  // Define your atoms (simple field initializers)
  viewIcon = jotai.atom<string>("circle");
  viewName = jotai.atom<string>("My View");
  noPadding = jotai.atom<boolean>(true);

  // Derived atom (created in constructor)
  viewText!: jotai.Atom<HeaderElem[]>;

  constructor(blockId: string, nodeModel: BlockNodeModel) {
    this.viewType = "myview";
    this.blockId = blockId;
    this.nodeModel = nodeModel;
    this.blockAtom = WOS.getWaveObjectAtom<Block>(`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.)
  }
}
为你的视图模型创建新文件(例如
frontend/app/view/myview/myview-model.ts
):
typescript
import { BlockNodeModel } from "@/app/block/blocktypes";
import { WOS, globalStore, 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.Atom<Block>;

  // Define your atoms (simple field initializers)
  viewIcon = jotai.atom<string>("circle");
  viewName = jotai.atom<string>("My View");
  noPadding = jotai.atom<boolean>(true);

  // Derived atom (created in constructor)
  viewText!: jotai.Atom<HeaderElem[]>;

  constructor(blockId: string, nodeModel: BlockNodeModel) {
    this.viewType = "myview";
    this.blockId = blockId;
    this.nodeModel = nodeModel;
    this.blockAtom = WOS.getWaveObjectAtom<Block>(`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.)
  }
}

2. Create the View Component

2. 创建视图组件

Create your React component (e.g.,
frontend/app/view/myview/myview.tsx
):
typescript
import { ViewComponentProps } from "@/app/block/blocktypes";
import { MyViewModel } from "./myview-model";
import { useAtomValue } from "jotai";
import "./myview.scss";

export const MyView: React.FC<ViewComponentProps<MyViewModel>> = ({
    blockId,
    model,
    contentRef
}) => {
    // Use atoms from the model (these are React hooks - call at top level!)
    const blockData = useAtomValue(model.blockAtom);

    return (
        <div className="myview-container" ref={contentRef}>
            <div>Block ID: {blockId}</div>
            <div>View: {model.viewType}</div>
            {/* Your view content here */}
        </div>
    );
};
创建你的React组件(例如
frontend/app/view/myview/myview.tsx
):
typescript
import { ViewComponentProps } from "@/app/block/blocktypes";
import { MyViewModel } from "./myview-model";
import { useAtomValue } from "jotai";
import "./myview.scss";

export const MyView: React.FC<ViewComponentProps<MyViewModel>> = ({
    blockId,
    model,
    contentRef
}) => {
    // Use atoms from the model (these are React hooks - call at top level!)
    const blockData = useAtomValue(model.blockAtom);

    return (
        <div className="myview-container" ref={contentRef}>
            <div>Block ID: {blockId}</div>
            <div>View: {model.viewType}</div>
            {/* Your view content here */}
        </div>
    );
};

3. Register the View

3. 注册视图

Add your view to the
BlockRegistry
in
frontend/app/block/block.tsx
:
typescript
const BlockRegistry: Map<string, 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
The registry key (e.g.,
"myview"
) becomes the view type used in block metadata.
frontend/app/block/block.tsx
BlockRegistry
中添加你的视图:
typescript
const BlockRegistry: Map<string, 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"
)将成为区块元数据中使用的视图类型。

4. Create Blocks with Your View

4. 使用你的视图创建区块

Users can create blocks with your view type:
  • Via CLI:
    wsh view myview
  • Via RPC: Use the block's
    meta.view
    field set to
    "myview"
用户可以使用你的视图类型创建区块:
  • 通过CLI:
    wsh view myview
  • 通过RPC:将区块的
    meta.view
    字段设置为
    "myview"

Real-World Examples

实际示例

Example 1: Terminal View (
term-model.ts
)

示例1:终端视图(
term-model.ts

The terminal view demonstrates:
  • Connection management via
    manageConnection
    atom
  • Dynamic header buttons showing shell status (play/restart)
  • Mode switching between terminal and vdom views
  • Custom keyboard handling for terminal-specific shortcuts
  • Focus management to focus the xterm.js instance
  • Shell integration status showing AI capability indicators
Key features:
typescript
this.manageConnection = jotai.atom((get) => {
  const termMode = get(this.termMode);
  if (termMode == "vdom") return false;
  return true; // Show connection picker for regular terminal mode
});

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;
});
终端视图展示了:
  • 通过
    manageConnection
    原子实现的连接管理
  • 显示shell状态(运行/重启)的动态头部按钮
  • 在终端视图和vdom视图之间的模式切换
  • 针对终端特定快捷键的自定义键盘处理
  • 聚焦xterm.js实例的焦点管理
  • 显示AI功能指示器的Shell集成状态
核心特性:
typescript
this.manageConnection = jotai.atom((get) => {
  const termMode = get(this.termMode);
  if (termMode == "vdom") return false;
  return true; // Show connection picker for regular terminal mode
});

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;
});

Example 2: Web View (
webview.tsx
)

示例2:Web视图(
webview.tsx

The web view shows:
  • Complex header controls (back/forward/home/URL input)
  • State management for loading, URL, and navigation
  • Event handling for webview navigation events
  • Custom styling with
    noPadding
    for full-bleed content
  • Media controls showing play/pause/mute when media is active
Key features:
typescript
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;
});
Web视图展示了:
  • 复杂头部控件(后退/前进/主页/URL输入)
  • 针对加载状态、URL和导航的状态管理
  • 针对webview导航事件的事件处理
  • 使用
    noPadding
    实现的自定义样式,用于全宽内容
  • 媒体播放时显示播放/暂停/静音的媒体控件
核心特性:
typescript
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;
});

Header Elements (
HeaderElem
)

头部元素(
HeaderElem

The
viewText
atom can return an array of these element types:
typescript
// Icon button
{
    elemtype: "iconbutton",
    icon: "refresh",
    title: "Tooltip text",
    click: () => { /* handler */ },
    disabled?: boolean,
    iconColor?: string,
    iconSpin?: boolean,
    noAction?: boolean,  // Shows icon but no click action
}

// Text element
{
    elemtype: "text",
    text: "Display text",
    className?: string,
    noGrow?: boolean,
    ref?: React.RefObject<HTMLElement>,
    onClick?: (e: React.MouseEvent) => void,
}

// Text button
{
    elemtype: "textbutton",
    text: "Button text",
    className?: string,
    title: "Tooltip",
    onClick: (e: React.MouseEvent) => void,
}

// Input field
{
    elemtype: "input",
    value: string,
    className?: string,
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
    onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void,
    onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void,
    onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void,
    ref?: React.RefObject<HTMLInputElement>,
}

// Container with children
{
    elemtype: "div",
    className?: string,
    children: HeaderElem[],
    onMouseOver?: (e: React.MouseEvent) => void,
    onMouseOut?: (e: React.MouseEvent) => void,
}

// Menu button (dropdown)
{
    elemtype: "menubutton",
    // ... MenuButtonProps ...
}
viewText
原子可以返回以下元素类型的数组:
typescript
// Icon button
{
    elemtype: "iconbutton",
    icon: "refresh",
    title: "Tooltip text",
    click: () => { /* handler */ },
    disabled?: boolean,
    iconColor?: string,
    iconSpin?: boolean,
    noAction?: boolean,  // Shows icon but no click action
}

// Text element
{
    elemtype: "text",
    text: "Display text",
    className?: string,
    noGrow?: boolean,
    ref?: React.RefObject<HTMLElement>,
    onClick?: (e: React.MouseEvent) => void,
}

// Text button
{
    elemtype: "textbutton",
    text: "Button text",
    className?: string,
    title: "Tooltip",
    onClick: (e: React.MouseEvent) => void,
}

// Input field
{
    elemtype: "input",
    value: string,
    className?: string,
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
    onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void,
    onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void,
    onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void,
    ref?: React.RefObject<HTMLInputElement>,
}

// Container with children
{
    elemtype: "div",
    className?: string,
    children: HeaderElem[],
    onMouseOver?: (e: React.MouseEvent) => void,
    onMouseOut?: (e: React.MouseEvent) => void,
}

// Menu button (dropdown)
{
    elemtype: "menubutton",
    // ... MenuButtonProps ...
}

Best Practices

最佳实践

Jotai Model Pattern

Jotai模型模式

Follow these rules for Jotai atoms in models:
  1. Simple atoms as field initializers:
    typescript
    viewIcon = jotai.atom<string>("circle");
    noPadding = jotai.atom<boolean>(true);
  2. Derived atoms in constructor (need dependency on other atoms):
    typescript
    constructor(blockId: string, nodeModel: BlockNodeModel) {
        this.viewText = jotai.atom((get) => {
            const blockData = get(this.blockAtom);
            return [/* computed based on blockData */];
        });
    }
  3. Models never use React hooks - Use
    globalStore.get()
    /
    set()
    :
    typescript
    refresh() {
        const currentData = globalStore.get(this.blockAtom);
        globalStore.set(this.dataAtom, newData);
    }
  4. Components use hooks for atoms:
    typescript
    const data = useAtomValue(model.dataAtom);
    const [value, setValue] = useAtom(model.valueAtom);
为模型中的Jotai原子遵循以下规则:
  1. 简单原子作为字段初始化器
    typescript
    viewIcon = jotai.atom<string>("circle");
    noPadding = jotai.atom<boolean>(true);
  2. 派生原子在构造函数中创建(依赖其他原子):
    typescript
    constructor(blockId: string, nodeModel: BlockNodeModel) {
        this.viewText = jotai.atom((get) => {
            const blockData = get(this.blockAtom);
            return [/* computed based on blockData */];
        });
    }
  3. 模型绝不使用React hooks - 使用
    globalStore.get()
    /
    set()
    typescript
    refresh() {
        const currentData = globalStore.get(this.blockAtom);
        globalStore.set(this.dataAtom, newData);
    }
  4. 组件使用hooks访问原子
    typescript
    const data = useAtomValue(model.dataAtom);
    const [value, setValue] = useAtom(model.valueAtom);

State Management

状态管理

  • All view state should live in atoms on the model
  • Use
    useBlockAtom()
    helper for block-scoped atoms that persist
  • Use
    globalStore
    for imperative access outside React components
  • Subscribe to Wave events using
    waveEventSubscribe()
  • 所有视图状态应存储在模型的原子中
  • 使用
    useBlockAtom()
    助手函数创建可持久化的区块作用域原子
  • 使用
    globalStore
    在React组件外进行命令式访问
  • 使用
    waveEventSubscribe()
    订阅Wave事件

Styling

样式

  • Create a
    .scss
    file for your view styles
  • Use Tailwind utilities where possible (v4)
  • Add
    noPadding: atom(true)
    for full-bleed content
  • Use
    blockBg
    atom to customize block background
  • 为你的视图创建
    .scss
    样式文件
  • 尽可能使用Tailwind工具类(v4版本)
  • 设置
    noPadding: atom(true)
    以实现全宽内容
  • 使用
    blockBg
    原子自定义区块背景

Focus Management

焦点管理

Implement
giveFocus()
to focus your view when:
  • Block gains focus via keyboard navigation
  • User clicks the block
  • Return
    true
    if successfully focused,
    false
    otherwise
实现
giveFocus()
以在以下场景中聚焦你的视图:
  • 区块通过键盘导航获得焦点时
  • 用户点击区块时
  • 成功聚焦返回
    true
    ,否则返回
    false

Keyboard Handling

键盘处理

Implement
keyDownHandler(e: WaveKeyboardEvent)
for:
  • View-specific keyboard shortcuts
  • Return
    true
    if event was handled (prevents propagation)
  • Use
    keyutil.checkKeyPressed(waveEvent, "Cmd:K")
    for shortcut checks
实现
keyDownHandler(e: WaveKeyboardEvent)
以支持:
  • 视图特定的键盘快捷键
  • 处理事件后返回
    true
    (阻止事件传播)
  • 使用
    keyutil.checkKeyPressed(waveEvent, "Cmd:K")
    检查快捷键

Cleanup

清理

Implement
dispose()
to:
  • Unsubscribe from Wave events
  • Unregister routes/handlers
  • Clear timers/intervals
  • Release resources
实现
dispose()
以:
  • 取消订阅Wave事件
  • 注销路由/处理器
  • 清除计时器/间隔任务
  • 释放资源

Connection Management

连接管理

For views that need remote connections:
typescript
this.manageConnection = jotai.atom(true); // Show connection picker
this.filterOutNowsh = jotai.atom(true); // Hide nowsh connections
Access connection status:
typescript
const connStatus = jotai.atom((get) => {
  const blockData = get(this.blockAtom);
  const connName = blockData?.meta?.connection;
  return get(getConnStatusAtom(connName));
});
对于需要远程连接的视图:
typescript
this.manageConnection = jotai.atom(true); // Show connection picker
this.filterOutNowsh = jotai.atom(true); // Hide nowsh connections
访问连接状态:
typescript
const connStatus = jotai.atom((get) => {
  const blockData = get(this.blockAtom);
  const connName = blockData?.meta?.connection;
  return get(getConnStatusAtom(connName));
});

Common Patterns

常见模式

Reading Block Metadata

读取区块元数据

typescript
import { getBlockMetaKeyAtom } from "@/store/global";

// In constructor:
this.someFlag = getBlockMetaKeyAtom(blockId, "myview:flag");

// In component:
const flag = useAtomValue(model.someFlag);
typescript
import { getBlockMetaKeyAtom } from "@/store/global";

// In constructor:
this.someFlag = getBlockMetaKeyAtom(blockId, "myview:flag");

// In component:
const flag = useAtomValue(model.someFlag);

Configuration Overrides

配置覆盖

Wave has a hierarchical config system (global → connection → block):
typescript
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;
});
Wave具有分层配置系统(全局 → 连接 → 区块):
typescript
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;
});

Updating Block Metadata

更新区块元数据

typescript
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 },
});
typescript
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 },
});

Additional Resources

额外资源

  • frontend/app/block/blockframe-header.tsx
    - Block header rendering
  • frontend/app/view/term/term-model.ts
    - Complex view example
  • frontend/app/view/webview/webview.tsx
    - Navigation UI example
  • frontend/types/custom.d.ts
    - Type definitions
  • frontend/app/block/blockframe-header.tsx
    - 区块头部渲染逻辑
  • frontend/app/view/term/term-model.ts
    - 复杂视图示例
  • frontend/app/view/webview/webview.tsx
    - 导航UI示例
  • frontend/types/custom.d.ts
    - 类型定义