waveterm/aiprompts/view-prompt.md
Mike Sawka a9db209218
Big Paste Handler Cleanup (#2535)
Handle both types of paste data. Write utility functions to normalize
paste events to {text; image}[]. Fix duplication issue (call
preventDefault() early). Handle multiple image pasting (by adding a
slight delay). Convert Ctrl:Shift:v to use a *native paste* which allows
capturing of images!
2025-11-07 18:15:49 -08:00

7.3 KiB

Wave Terminal ViewModel Guide

Overview

Wave Terminal uses a modular ViewModel system to define interactive blocks. Each block has a ViewModel, which manages its metadata, configuration, and state using Jotai atoms. The ViewModel also specifies a React component (ViewComponent) that renders the block.

Key Concepts

  1. ViewModel Structure

    • Implements the ViewModel interface.
    • Defines:
      • viewType: Unique block type identifier.
      • viewIcon, viewName, viewText: Atoms for UI metadata.
      • preIconButton, endIconButtons: Atoms for action buttons.
      • blockBg: Atom for background styling.
      • manageConnection, noPadding, searchAtoms.
      • viewComponent: React component rendering the block.
      • Lifecycle methods like dispose(), giveFocus(), keyDownHandler().
  2. ViewComponent Structure

    • A React function component implementing ViewComponentProps<T extends ViewModel>.
    • Uses blockId, blockRef, contentRef, and model as props.
    • Retrieves ViewModel state using Jotai atoms.
    • Returns JSX for rendering.
  3. Header Elements (HeaderElem[])

    • Can include:
      • Icons (IconButtonDecl): Clickable buttons.
      • Text (HeaderText): Metadata or status.
      • Inputs (HeaderInput): Editable fields.
      • Menu Buttons (MenuButton): Dropdowns.
  4. Jotai Atoms for State Management

    • Use atom<T>, PrimitiveAtom<T>, WritableAtom<T> for dynamic properties.
    • splitAtom for managing lists of atoms.
    • Read settings from globalStore and override with block metadata.
  5. Metadata vs. Global Config

    • Block Metadata (SetMetaCommand): Each block persists its own configuration in its metadata (blockAtom.meta).
    • Global Config (SetConfigCommand): Provides default settings for all blocks, stored in config files.
    • Cascading Behavior:
      • Blocks first check their own metadata for settings.
      • If no override exists, they fall back to global config.
      • Updating a block's setting is done via SetMetaCommand (persisted per block).
      • Updating a global setting is done via SetConfigCommand (applies globally unless overridden).
  6. Useful Helper Functions

    • To avoid repetitive boilerplate, use these global utilities from global.ts:
      • useBlockMetaKeyAtom(blockId, key): Retrieves and updates block-specific metadata.
      • useOverrideConfigAtom(blockId, key): Reads from global config but allows per-block overrides.
      • useSettingsKeyAtom(key): Accesses global settings efficiently.
  7. Styling

    • Use TailWind CSS to style components
    • Accent color is: text-accent, for a 50% transparent accent background use bg-accentbg
    • Hover background is: bg-hoverbg
    • Border color is "border", so use border-border
    • Colors are also defined for error, warning, and success (text-error, text-warning, text-sucess)

Relevant TypeScript Types

type ViewComponentProps<T extends ViewModel> = {
  blockId: string;
  blockRef: React.RefObject<HTMLDivElement>;
  contentRef: React.RefObject<HTMLDivElement>;
  model: T;
};

type ViewComponent = React.FC<ViewComponentProps<any>>;

interface ViewModel {
  viewType: string;
  viewIcon?: jotai.Atom<string | IconButtonDecl>;
  viewName?: jotai.Atom<string>;
  viewText?: jotai.Atom<string | HeaderElem[]>;
  preIconButton?: jotai.Atom<IconButtonDecl>;
  endIconButtons?: jotai.Atom<IconButtonDecl[]>;
  blockBg?: jotai.Atom<MetaType>;
  manageConnection?: jotai.Atom<boolean>;
  noPadding?: jotai.Atom<boolean>;
  searchAtoms?: SearchAtoms;
  viewComponent: ViewComponent;
  dispose?: () => void;
  giveFocus?: () => boolean;
  keyDownHandler?: (e: WaveKeyboardEvent) => boolean;
}

interface IconButtonDecl {
  elemtype: "iconbutton";
  icon: string | React.ReactNode;
  click?: (e: React.MouseEvent<any>) => void;
}
type HeaderElem =
  | IconButtonDecl
  | ToggleIconButtonDecl
  | HeaderText
  | HeaderInput
  | HeaderDiv
  | HeaderTextButton
  | ConnectionButton
  | MenuButton;

type IconButtonCommon = {
  icon: string | React.ReactNode;
  iconColor?: string;
  iconSpin?: boolean;
  className?: string;
  title?: string;
  disabled?: boolean;
  noAction?: boolean;
};

type IconButtonDecl = IconButtonCommon & {
  elemtype: "iconbutton";
  click?: (e: React.MouseEvent<any>) => void;
  longClick?: (e: React.MouseEvent<any>) => void;
};

type ToggleIconButtonDecl = IconButtonCommon & {
  elemtype: "toggleiconbutton";
  active: jotai.WritableAtom<boolean, [boolean], void>;
};

type HeaderTextButton = {
  elemtype: "textbutton";
  text: string;
  className?: string;
  title?: string;
  onClick?: (e: React.MouseEvent<any>) => void;
};

type HeaderText = {
  elemtype: "text";
  text: string;
  ref?: React.RefObject<HTMLDivElement>;
  className?: string;
  noGrow?: boolean;
  onClick?: (e: React.MouseEvent<any>) => void;
};

type HeaderInput = {
  elemtype: "input";
  value: string;
  className?: string;
  isDisabled?: boolean;
  ref?: React.RefObject<HTMLInputElement>;
  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
  onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
  onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
  onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
};

type HeaderDiv = {
  elemtype: "div";
  className?: string;
  children: HeaderElem[];
  onMouseOver?: (e: React.MouseEvent<any>) => void;
  onMouseOut?: (e: React.MouseEvent<any>) => void;
  onClick?: (e: React.MouseEvent<any>) => void;
};

type ConnectionButton = {
  elemtype: "connectionbutton";
  icon: string;
  text: string;
  iconColor: string;
  onClick?: (e: React.MouseEvent<any>) => void;
  connected: boolean;
};

type MenuItem = {
  label: string;
  icon?: string | React.ReactNode;
  subItems?: MenuItem[];
  onClick?: (e: React.MouseEvent<any>) => void;
};

type MenuButtonProps = {
  items: MenuItem[];
  className?: string;
  text: string;
  title?: string;
  menuPlacement?: Placement;
};

type MenuButton = {
  elemtype: "menubutton";
} & MenuButtonProps;

Minimal "Hello World" Example

This example defines a simple ViewModel and ViewComponent for a block that displays "Hello, World!".

import * as jotai from "jotai";
import React from "react";

class HelloWorldModel implements ViewModel {
    viewType = "helloworld";
    viewIcon = jotai.atom("smile");
    viewName = jotai.atom("Hello World");
    viewText = jotai.atom("A simple greeting block");
    viewComponent = HelloWorldView;
}

const HelloWorldView: ViewComponent<HelloWorldModel> = ({ model }) => {
    return <div style={{ padding: "10px" }}>Hello, World!</div>;
};

export { HelloWorldModel };

Instructions to AI

  1. Generate a new ViewModel class for a block, following the structure above.
  2. Generate a corresponding ViewComponent.
  3. Use Jotai atoms to store all dynamic state.
  4. Ensure the ViewModel defines header elements (viewText, viewIcon, endIconButtons).
  5. Export the view model (to be registered in the BlockRegistry)
  6. Use existing metadata patterns for config and settings.

Other Notes

  • The types you see above don't need to be imported, they are global types (custom.d.ts)

Output Format:

  • TypeScript code defining the ViewModel.
  • TypeScript code defining the ViewComponent.
  • Ensure alignment with the patterns in waveai.tsx, preview.tsx, sysinfo.tsx, and term.tsx.