Develop Tsunami widgets for Wave Terminal. This skill provides progressive disclosure from basics (what is Wave/Tsunami, component model) through intermediate (atoms, vdom, hooks) to advanced (RPC, AI integration, WSH commands). Use when building Wave widgets, working with the Tsunami framework, or integrating with Wave's AI capabilities.
Wave Terminal is an open-source, AI-native terminal that combines traditional terminal features with graphical capabilities. The Tsunami framework enables custom widget development in Go with React-like patterns.
| Level | Topic | Reference File |
|---|---|---|
| Basic | Architecture overview | references/basics.md |
| Basic | Component patterns | references/components.md |
| Intermediate | State management | references/state.md |
| Intermediate | Virtual DOM | references/vdom.md |
| Complete | Full API Reference | references/api-complete.md |
| Widget Registration |
references/widget-registration.md |
| Advanced | AI integration | references/ai-integration.md |
| Advanced | WSH commands | references/wsh.md |
| CogOS | Cog-native widgets | references/cog-widgets.md |
~/.config/waveterm/~/.config/waveterm/widgets.json# Using tsunami-build wrapper
./scripts/tsunami-build widgets/my-widget # Build
./scripts/tsunami-build widgets/my-widget --run # Build & run
# Using tsunami CLI directly
export TSUNAMI_SCAFFOLDPATH=/path/to/wave-terminal/dist/tsunamiscaffold
export TSUNAMI_SDKREPLACEPATH=/path/to/wave-terminal/tsunami
tsunami build /path/to/widget -o /path/to/widget/widget-name
# Launch a registered widget
wsh launch cog-spark # By widget ID
wsh launch cog-chat -m # Magnified mode
# Claude can launch widgets directly!
# Just use: wsh launch <widget-id>
Add to ~/.config/waveterm/widgets.json:
{
"my-widget": {
"icon": "zap",
"label": "my-widget",
"color": "#fbbf24",
"description": "Widget description",
"display:order": 0,
"blockdef": {
"meta": {
"view": "tsunami",
"controller": "tsunami",
"tsunami:apppath": "/path/to/widget/directory",
"tsunami:env": {
"COG_WORKSPACE": "/Users/slowbro/cog-workspace"
}
}
}
}
}
Icon options: See Lucide Icons - use zap, hexagon, message-square, sparkles, etc.
// ConfigAtom - User-configurable (appears in widget settings)
var intervalAtom = app.ConfigAtom("interval", 30, &app.AtomMeta{
Desc: "Refresh interval", Min: app.Ptr(5.0), Max: app.Ptr(300.0),
})
// DataAtom - Runtime data (persisted, external access)
var dataAtom = app.DataAtom("data", []Item{}, nil)
// UseLocal - Component-scoped (auto-cleanup)
inputAtom := app.UseLocal("")
app.UseLocal(initial) // Local state
app.UseRef(initial) // Mutable ref (no re-render)
app.UseEffect(fn, deps) // Side effects
app.UseTicker(interval, fn, deps) // Periodic callback
app.UseId() // Component UUID
For cog-native widgets that integrate with the workspace substrate:
# Build a widget
./scripts/tsunami-build widgets/cog-eidolon
# Build and run standalone
./scripts/tsunami-build widgets/cog-chat --run
Load references/cog-widgets.md when:
Go Backend (Tsunami)
├── Component definitions (React-like)
├── Virtual DOM rendering engine
├── State management (atoms + hooks)
└── RPC streaming to frontend
↓
Frontend (TypeScript)
├── Renders VDom sent from backend
├── Event handling (clicks, keyboard)
└── Sends events back to backend
package main
import (
"github.com/wavetermdev/waveterm/tsunami/app"
"github.com/wavetermdev/waveterm/tsunami/vdom"
)
var AppMeta = app.AppMeta{
Title: "My Widget",
ShortDesc: "Widget description",
}
var App = app.DefineComponent("App", func(_ struct{}) any {
return vdom.H("div", map[string]any{
"className": "p-4",
}, "Hello from Tsunami!")
})
widget-name/
├── app.go # Main component definitions
├── go.mod # Go module (tsunami/app/widget-name)
├── go.sum # Dependencies
├── manifest.json # Widget metadata for Wave
└── static/ # Optional static assets
Load references/basics.md when:
Load references/components.md when:
DefineComponentLoad references/state.md when:
UseAsync, UseLocalLoad references/vdom.md when:
vdom.H()Load references/ai-integration.md when:
Load references/wsh.md when:
var Counter = app.DefineComponent("Counter", func(_ struct{}) any {
countAtom := app.UseLocal(0)
return vdom.H("div", nil,
vdom.H("span", nil, fmt.Sprintf("Count: %d", countAtom.Get())),
vdom.H("button", map[string]any{
"onClick": func() { countAtom.Set(countAtom.Get() + 1) },
}, "Increment"),
)
})
var DataList = app.DefineComponent("DataList", func(_ struct{}) any {
dataAtom := app.UseAsync(func() []Item {
// Fetch data (runs in goroutine)
return fetchItems()
})
items := dataAtom.Get()
if items == nil {
return vdom.H("div", nil, "Loading...")
}
return vdom.H("ul", nil,
// Map over items...
)
})
vdom.H("input", map[string]any{
"type": "text",
"value": inputAtom.Get(),
"onChange": func(e vdom.VDomEvent) { inputAtom.Set(e.TargetValue) },
"onKeyDown": func(e vdom.VDomEvent) {
if e.KeyData != nil && e.KeyData.Key == "Enter" {
handleSubmit()
}
},
})
Wave's block-content container has position: relative, which enables a key layout pattern:
Problem: h-full doesn't work because Wave uses min-h-full on the VDom container.
Solution: Use absolute inset-0 to fill the positioned ancestor:
var App = app.DefineComponent("App", func(_ struct{}) any {
return vdom.H("div", map[string]any{
// absolute inset-0 fills the block-content container
"className": "absolute inset-0 flex flex-col overflow-hidden bg-gray-950",
},
// Widget content...
)
})
For chat-style UIs with fixed header and input at bottom:
return vdom.H("div", map[string]any{
"className": "absolute inset-0 flex flex-col overflow-hidden",
},
// Header - pinned to top
vdom.H("div", map[string]any{
"className": "flex-shrink-0 p-2 border-b border-gray-800",
}, "Header"),
// Middle - scrollable content area
vdom.H("div", map[string]any{
"className": "flex-1 min-h-0 overflow-y-auto p-4",
},
// Scrollable content here
),
// Footer - pinned to bottom
vdom.H("div", map[string]any{
"className": "flex-shrink-0 p-2 border-t border-gray-800",
}, "Footer/Input"),
)
Key CSS classes:
absolute inset-0 - Fill the positioned parent (block-content)flex-shrink-0 - Prevent header/footer from shrinkingflex-1 min-h-0 - Allow middle to fill remaining space and enable scrollingoverflow-y-auto - Enable vertical scrolling in middle sectionoverflow-hidden - Prevent content overflow on rootWave provides a built-in markdown component:
vdom.H("wave:markdown", map[string]any{
"text": "# Hello **World**\n\nThis is markdown.",
"className": "prose prose-invert",
"scrollable": false, // true for scrollable markdown blocks
})
Uses react-markdown on the frontend. No external Go libraries needed.
| Issue | Solution |
|---|---|
| Widget not rendering | Check AppMeta and App are exported (capitalized) |
| State not updating | Ensure using .Set() method on atoms |
| Events not firing | Verify event handler is a func() or func(vdom.VDomEvent) |
| Imports failing | Check go.mod references correct tsunami paths |
| Widget not filling pane height | Use absolute inset-0 instead of h-full on root |
| Header/footer not pinned | Use flex layout with flex-shrink-0 on pinned elements |
| Scrolling not working | Add min-h-0 to scrollable container (flex quirk) |
go.mod with tsunami/app/widget-name module pathapp.go with AppMeta and App componentmanifest.json with widget metadata