Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a8df896
get a tsunami builder window stubbed out
sawka Oct 26, 2025
393d3ae
fixed devtools, window size, and reload
sawka Oct 26, 2025
9f33cdc
create builder route in wshrpc. /ws route now uses routeid instead o…
sawka Oct 26, 2025
025c0eb
Merge remote-tracking branch 'origin/main' into sawka/tsunami-builder
sawka Oct 26, 2025
4b78b9e
builder windows register a different set of global keys. hook up glo…
sawka Oct 26, 2025
207b0ed
add a panel (resizable) layout to the builder window...
sawka Oct 26, 2025
b3acddd
Merge remote-tracking branch 'origin/main' into sawka/tsunami-builder
sawka Oct 26, 2025
3f805a6
rename blockrtinfo to rtinfo store and objrtinfo... since we use for …
sawka Oct 26, 2025
163b11b
save and restore builder layouts
sawka Oct 26, 2025
f429f97
refactor tabatom cache to be oref cache... and fold block atom cache …
sawka Oct 26, 2025
2b2192a
refactor wave ai model to use orefContext instead of static tab id
sawka Oct 26, 2025
1618e7f
small feedback button refactor to use model
sawka Oct 26, 2025
aea51ca
Merge remote-tracking branch 'origin/main' into sawka/tsunami-builder
sawka Oct 26, 2025
0317815
move most backend dependencies to the model
sawka Oct 26, 2025
f3f260e
add wave window type atom
sawka Oct 26, 2025
3bc0ba2
get the aipanel to show inside of the builder
sawka Oct 26, 2025
f16a94d
fixing integration issues with waveai in the builder
sawka Oct 26, 2025
7933dda
focusManager now uses getInstance() pattern. also create a stubbed o…
sawka Oct 26, 2025
859403d
fixing waveai focus
sawka Oct 26, 2025
a95629e
fix focus in builder
sawka Oct 26, 2025
12e7a81
manage local apps + drafts. some builder tools for writing app.go
sawka Oct 26, 2025
2a81fd3
create builder tabs...
sawka Oct 26, 2025
3c0d927
bug fixes
sawka Oct 26, 2025
e62372b
better focus handling for wave ai vs preview tab...
sawka Oct 27, 2025
02dc42f
remove global var
sawka Oct 27, 2025
e09127d
fix parseDataUrl
sawka Oct 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 261 additions & 0 deletions aiprompts/tsunami-builder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
# Tsunami AI Builder - V1 Architecture

## Overview

A split-screen builder for creating Tsunami applications: chat interface on left, tabbed preview/code/files on right. Users describe what they want, AI edits the code iteratively.

## UI Layout

### Left Panel

- **💬 Chat** - Conversation with AI

### Right Panel

**Top Section - Tabs:**
- **👁️ Preview** (default) - Live preview of running Tsunami app, updates automatically after successful compilation
- **📝 Code** - Monaco editor for manual edits to app.go
- **📁 Files** - Static assets browser (images, etc)

**Bottom Section - Build Panel (closable):**
- Shows compilation status and output (like VSCode's terminal panel)
- Displays success messages or errors with line numbers
- Auto-runs after AI edits
- For manual Code tab edits: auto-reruns or user clicks build button
- Can be manually closed/reopened by user

### Top Bar

- Current AppTitle (extracted from app.go)
- **Publish** button - Moves draft → published version
- **Revert** button - Copies published → draft (discards draft changes)

## Version Management

**Draft mode**: Auto-saved on every edit, persists when builder closes
**Published version**: What runs in main Wave Terminal, only updates on explicit "Publish"

Flow:

1. Edit in builder (always editing draft)
2. Click "Publish" when ready (copies draft → published)
3. Continue editing draft OR click "Revert" to abandon changes

## Context Structure

Every AI request includes:

```
[System Instructions]
- General system prompt
- Full system.md (Tsunami framework guide)

[Conversation History]
- Recent messages (with prompt caching)

[Current Context] (injected fresh each turn, removed from previous turns)
- Current app.go content
- Compilation results (success or errors with line numbers)
- Static files listing (e.g., "/static/logo.png")
```

**Context cleanup**: Old "current context" blocks are removed from previous messages and replaced with "[OLD CONTEXT REMOVED]" to save tokens. Only the latest app.go + compile results stay in context.

## AI Tools

### edit_appgo (str_replace)

**Primary editing tool**

- `old_str` - Unique string to find in app.go
- `new_str` - Replacement string
- `description` - What this change does

**Backend behavior**:

1. Apply string replacement to app.go
2. Immediately run `go build`
3. Return tool result:
- ✓ Success: "Edit applied, compilation successful"
- ✗ Failure: "Edit applied, compilation failed: [error details]"

AI can make multiple edits in one response, getting compile feedback after each.

### create_appgo

**Bootstrap new apps**

- `content` - Full app.go file content
- Only used for initial app creation or total rewrites

Same compilation behavior as str_replace.

### web_search

**Look up APIs, docs, examples**

- Implemented via provider backend (OpenAI/Anthropic)
- AI can research before making edits

### read_file

**Read user-provided documentation**

- `path` - Path to file (e.g., "/docs/api-spec.md")
- User can upload docs/examples for AI to reference

## User Actions (Not AI Tools)

### Manage Static Assets

- Upload via drag & drop into Files tab or file picker
- Delete files from Files tab
- Rename files from Files tab
- Appear in `/static/` directory
- Auto-injected into AI context as available files

### Share Screenshot

- User clicks "📷 Share preview with AI" button
- Captures current preview state
- Attaches to user's next message
- Useful for debugging layout/visual issues

### Manual Code Editing

- User can switch to Code tab
- Edit app.go directly in Monaco editor
- Changes auto-compile
- AI sees manual edits in next chat turn

## Compilation Pipeline

After every code change (AI or user):

```
1. Write app.go to disk
2. Run: go build app.go
3. Show build output in build panel
4. If success:
- Start/restart app process
- Update preview iframe
- Show success message in build panel
5. If failure:
- Parse error output (line numbers, messages)
- Show error in build panel (bottom of right side)
- Inject into AI context for next turn
```

**Auto-retry**: AI can fix its own compilation errors within the same response (up to 3 attempts).

## Error Handling

### Compilation Errors

Shown in build panel at bottom of right side.

Format for AI:

```
COMPILATION FAILED

Error at line 45:
43 | func(props TodoProps) any {
44 | return vdom.H("div", nil
> 45 | vdom.H("span", nil, "test")
| ^ missing closing parenthesis
46 | )

Message: expected ')', found 'vdom'
```

### Runtime Errors

- Shown in preview tab (not errors panel)
- User can screenshot and report to AI
- Not auto-injected (v1 simplification)

### Linting (Future)

- Could add custom Tsunami-specific linting
- Would inject warnings alongside compile results
- Not required for v1

## Secrets/Configuration

Apps can declare secrets using Tsunami's ConfigAtom:

```go
var apiKeyAtom = app.ConfigAtom("api_key", "", &app.AtomMeta{
Desc: "OpenAI API Key",
Secret: true,
})
```

Builder detects these and shows input fields in UI for user to fill in.

## Conversation Limits

**V1 approach**: No summarization, no smart handling.

When context limit hit: Show message "You've hit the conversation limit. Click 'Start Fresh' to continue editing this app in a new chat."

Starting fresh uses current app.go as the beginning state.

## Token Optimization

- System.md + early messages benefit from prompt caching
- Only pay per-turn for: current app.go + new messages
- Old context blocks removed to prevent bloat
- Estimated: 10-20k tokens per turn (very manageable)

## Example Flow

```
User: "Create a counter app"
AI: [calls create_appgo with full counter app]
Backend: ✓ Compiled successfully
Preview: Shows counter app

User: "Add a reset button"
AI: [calls str_replace to add reset button]
Backend: ✓ Compiled successfully
Preview: Updates with reset button

User: "Make buttons bigger"
AI: [calls str_replace to update button classes]
Backend: ✓ Compiled successfully
Preview: Updates with larger buttons

User: [switches to Code tab, tweaks color manually]
Backend: ✓ Compiled successfully
Preview: Updates

User: "Add a chart showing count over time"
AI: [calls web_search for "go charting library"]
AI: [calls str_replace to add chart]
Backend: ✗ Compilation failed - missing import
AI: [calls str_replace to add import]
Backend: ✓ Compiled successfully
Preview: Shows chart
```

## Out of Scope (V1)

- Version history / snapshots
- Multiple files / project structure
- Collaboration / sharing
- Advanced linting
- Runtime error auto-injection
- Conversation summarization
- Component-specific editing tools

These can be added in v2+ based on user feedback.

## Success Criteria

- User can create functional Tsunami app through chat in <5 minutes
- AI successfully fixes its own compilation errors 80%+ of the time
- Iteration cycle (message → edit → preview) takes <10 seconds
- Users can publish working apps to Wave Terminal
- Draft state persists across sessions
114 changes: 114 additions & 0 deletions emain/emain-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { ClientService } from "@/app/store/services";
import { RpcApi } from "@/app/store/wshclientapi";
import { randomUUID } from "crypto";
import { BrowserWindow } from "electron";
import { globalEvents } from "emain/emain-events";
import path from "path";
import { getElectronAppBasePath, isDevVite, unamePlatform } from "./emain-platform";
import { calculateWindowBounds, MinWindowHeight, MinWindowWidth } from "./emain-window";
import { ElectronWshClient } from "./emain-wsh";

export type BuilderWindowType = BrowserWindow & {
builderId: string;
savedInitOpts: BuilderInitOpts;
};

const builderWindows: BuilderWindowType[] = [];
export let focusedBuilderWindow: BuilderWindowType = null;

export function getBuilderWindowById(builderId: string): BuilderWindowType {
return builderWindows.find((win) => win.builderId === builderId);
}

export function getBuilderWindowByWebContentsId(webContentsId: number): BuilderWindowType {
return builderWindows.find((win) => win.webContents.id === webContentsId);
}

export function getAllBuilderWindows(): BuilderWindowType[] {
return builderWindows;
}

export async function createBuilderWindow(appId: string): Promise<BuilderWindowType> {
const builderId = randomUUID();

const fullConfig = await RpcApi.GetFullConfigCommand(ElectronWshClient);
const clientData = await ClientService.GetClientData();
const clientId = clientData?.oid;
const windowId = randomUUID();

const winBounds = calculateWindowBounds(undefined, undefined, fullConfig.settings);

const builderWindow = new BrowserWindow({
x: winBounds.x,
y: winBounds.y,
width: winBounds.width,
height: winBounds.height,
minWidth: MinWindowWidth,
minHeight: MinWindowHeight,
titleBarStyle: unamePlatform === "darwin" ? "hiddenInset" : "default",
icon:
unamePlatform === "linux"
? path.join(getElectronAppBasePath(), "public/logos/wave-logo-dark.png")
: undefined,
show: false,
backgroundColor: "#222222",
webPreferences: {
preload: path.join(getElectronAppBasePath(), "preload", "index.cjs"),
webviewTag: true,
},
});

if (isDevVite) {
await builderWindow.loadURL(`${process.env.ELECTRON_RENDERER_URL}/index.html`);
} else {
await builderWindow.loadFile(path.join(getElectronAppBasePath(), "frontend", "index.html"));
}

const initOpts: BuilderInitOpts = {
builderId,
clientId,
windowId,
appId,
};

const typedBuilderWindow = builderWindow as BuilderWindowType;
typedBuilderWindow.builderId = builderId;
typedBuilderWindow.savedInitOpts = initOpts;

console.log("sending builder-init", initOpts);
typedBuilderWindow.webContents.send("builder-init", initOpts);

typedBuilderWindow.on("focus", () => {
focusedBuilderWindow = typedBuilderWindow;
console.log("builder window focused", builderId);
setTimeout(() => globalEvents.emit("windows-updated"), 50);
});

typedBuilderWindow.on("blur", () => {
if (focusedBuilderWindow === typedBuilderWindow) {
focusedBuilderWindow = null;
}
setTimeout(() => globalEvents.emit("windows-updated"), 50);
});

typedBuilderWindow.on("closed", () => {
console.log("builder window closed", builderId);
const index = builderWindows.indexOf(typedBuilderWindow);
if (index !== -1) {
builderWindows.splice(index, 1);
}
if (focusedBuilderWindow === typedBuilderWindow) {
focusedBuilderWindow = null;
}
setTimeout(() => globalEvents.emit("windows-updated"), 50);
});

builderWindows.push(typedBuilderWindow);
typedBuilderWindow.show();

console.log("created builder window", builderId, appId);
return typedBuilderWindow;
}
Loading
Loading