diff --git a/aiprompts/waveapp.md b/aiprompts/waveapp.md new file mode 100644 index 0000000000..ce60450e3e --- /dev/null +++ b/aiprompts/waveapp.md @@ -0,0 +1,365 @@ +# Wave Apps Architecture Guide + +Wave Apps are self-contained web applications that run within Wave Terminal blocks. They combine a Node.js backend server with a React frontend, providing a standardized way to create interactive applications that integrate seamlessly with Wave Terminal's block system. + +## Project Structure + +A typical Wave App follows this directory structure: + +``` +waveapp-name/ +├── describe.json # App metadata and API specification +├── package.json # Node.js dependencies and scripts +├── server.js # Hono backend server (Node.js) +├── vite.config.ts # Vite build configuration +├── tsconfig.json # TypeScript configuration +├── index.html # HTML entry point +├── public/ # Static assets +└── src/ + ├── main.tsx # React app entry point + ├── index.css # Tailwind CSS styles + └── App.tsx # Main React component (optional) +``` + +## Technology Stack + +### Backend Framework +- **[Hono](https://hono.dev/)** - Fast, lightweight web framework for Node.js +- **[@hono/node-server](https://github.com/honojs/node-server)** - Node.js adapter for Hono +- **CORS support** via `hono/cors` middleware +- **Static file serving** via `@hono/node-server/serve-static` + +### Frontend Framework +- **[React 19](https://react.dev/)** - UI framework with modern features +- **[Vite 7](https://vitejs.dev/)** - Fast build tool and dev server +- **[TypeScript 5](https://www.typescriptlang.org/)** - Type-safe JavaScript +- **[@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react)** - React support for Vite + +### Styling +- **[Tailwind CSS v4](https://tailwindcss.com/)** - Utility-first CSS framework +- **[@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss-vite)** - Vite plugin for Tailwind +- **Custom CSS variables** - Wave Terminal theme integration + +### Development Tools +- **[concurrently](https://github.com/open-cli-tools/concurrently)** - Run backend and frontend simultaneously +- **TypeScript definitions** - Full type safety across the stack + +## Core Configuration Files + +### describe.json + +The [`describe.json`](waveapps/jwt/describe.json) file is the heart of every Wave App. It defines: + +- **App metadata** (name, version, description) +- **API specification** (endpoints, schemas, actions) +- **Configuration schema** for app settings +- **Data schema** for app state +- **Custom actions** the app can perform + +```json +{ + "name": "App Name", + "version": "1.0.0", + "baseurl": "/", + "description": "App description", + "actions": [ + { + "name": "action-name", + "method": "POST", + "path": "/api/action", + "description": "Action description", + "inputschema": "InputSchema", + "outputschema": "OutputSchema" + } + ], + "schemas": { + "config": { + "type": "object", + "description": "App configuration schema", + "properties": {} + }, + "data": { + "type": "object", + "description": "App data schema", + "properties": {} + } + } +} +``` + +### package.json Scripts + +Standard scripts for Wave App development: + +```json +{ + "scripts": { + "start": "node server.js", + "dev": "concurrently \"node server.js\" \"vite\"", + "build": "vite build", + "preview": "vite preview" + } +} +``` + +### vite.config.ts + +Vite configuration with Tailwind CSS and API proxy: + +```typescript +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + port: 5173, + proxy: { + "/api": "http://localhost:3000/", + }, + }, + build: { + outDir: "dist", + }, +}); +``` + +## Required API Endpoints + +Every Wave App **MUST** implement these three endpoints: + +### 1. `/api/config` (GET/PUT) + +**GET** - Returns current app configuration +**PUT** - Updates app configuration with new settings + +```javascript +// GET /api/config +app.get("/api/config", (c) => { + return c.json(appConfig); +}); + +// PUT /api/config +app.put("/api/config", async (c) => { + try { + const newConfig = await c.req.json(); + appConfig = { ...appConfig, ...newConfig }; + return c.json(appConfig); + } catch (error) { + return c.json({ error: "Invalid config format" }, 400); + } +}); +``` + +### 2. `/api/describe` (GET) + +Serves the [`describe.json`](waveapps/jwt/describe.json) file containing app metadata and API specification: + +```javascript +app.get("/api/describe", serveStatic({ path: "./describe.json" })); +``` + +### 3. `/api/data` (GET) + +Returns current app state/data for Wave Terminal integration: + +```javascript +app.get("/api/data", (c) => { + return c.json(appData || {}); +}); +``` + +## Server Architecture + +### Hono Server Setup + +Wave Apps use Hono as the backend framework with these key features: + +```javascript +import { serve } from "@hono/node-server"; +import { serveStatic } from "@hono/node-server/serve-static"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; + +const app = new Hono(); + +// CORS for frontend communication +app.use("/*", cors()); + +// Static file serving for built frontend +app.use("/static/*", serveStatic({ root: "./dist" })); +app.use("/*", serveStatic({ root: "./dist" })); + +// Start server on fixed port +const server = serve({ + fetch: app.fetch, + port: 3000, +}); +``` + +### Wave Terminal Integration + +Apps communicate with Wave Terminal through a messaging system: + +```javascript +function sendWaveAppMessage(message) { + if (process.send) { + process.send(message); + } else { + console.log(`#waveapp${JSON.stringify(message)}`); + } +} + +server.on("listening", () => { + const port = server.address().port; + sendWaveAppMessage({ + type: "listening", + port: port, + }); +}); +``` + +### State Management + +Apps maintain their own state for configuration and data: + +```javascript +let appData = null; // Current app state +let appConfig = {}; // App configuration +``` + +## Frontend Architecture + +### React Entry Point + +The frontend uses React 19 with TypeScript, typically structured as a single-page application: + +```typescript +// src/main.tsx +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +const App: React.FC = () => { + // App component logic + return ( +
+ {/* App UI */} +
+ ); +}; + +ReactDOM.createRoot(document.getElementById('root')!).render(); +``` + +### Tailwind CSS Integration + +Wave Apps use Tailwind CSS v4 with custom CSS variables for Wave Terminal theme integration: + +```css +/* src/index.css */ +@import "https://github.com/tailwindcss"; + +@theme { + --color-background: rgb(34, 34, 34); + --color-foreground: #f7f7f7; + --color-accent: rgb(88, 193, 66); + --color-panel: rgba(31, 33, 31, 0.5); + --color-border: rgba(255, 255, 255, 0.16); + /* ... more theme variables */ +} +``` + +### API Communication + +Frontend communicates with backend via standard fetch API: + +```typescript +const response = await fetch('https://github.com/api/endpoint', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data) +}); + +const result = await response.json(); +``` + +## Development Workflow + +### Development Mode + +Run both backend and frontend in development: + +```bash +npm run dev +# Runs: concurrently "node server.js" "vite" +``` + +This starts: +- **Backend server** on port 3000 (Hono) +- **Frontend dev server** on port 5173 (Vite) +- **API proxy** from frontend to backend + +### Production Build + +Build and serve the app: + +```bash +npm run build # Build frontend to dist/ +npm start # Start production server +``` + +### File Structure After Build + +``` +waveapp-name/ +├── dist/ # Built frontend files +│ ├── index.html +│ ├── assets/ +│ └── static/ +├── server.js # Backend server +└── describe.json # App metadata +``` + +## Integration with Wave Terminal + +Wave Apps integrate with Wave Terminal through: + +1. **Block System** - Apps run within Wave Terminal blocks +2. **Configuration** - Apps receive config from Wave Terminal via `/api/config` +3. **Data Exchange** - Apps expose state via `/api/data` +4. **Actions** - Apps define custom actions in `describe.json` +5. **Messaging** - Apps communicate status via the messaging system + +## Best Practices + +### Backend +- Use fixed ports (3000 for backend, 5173 for dev frontend) +- Implement proper error handling in API endpoints +- Maintain app state in memory (or persist as needed) +- Use CORS middleware for frontend communication +- Serve static files from the `dist` directory + +### Frontend +- Use Tailwind CSS with Wave Terminal theme variables +- Implement proper loading and error states +- Use TypeScript for type safety +- Follow React best practices and hooks patterns +- Handle API errors gracefully + +### Configuration +- Define clear schemas in `describe.json` +- Validate configuration inputs +- Provide sensible defaults +- Document all API endpoints and schemas + +### Development +- Use `concurrently` for simultaneous backend/frontend development +- Leverage Vite's hot reload for fast iteration +- Test both development and production builds +- Follow Wave Terminal's coding conventions + +This architecture provides a robust foundation for building interactive applications that integrate seamlessly with Wave Terminal's block-based interface while maintaining modern web development practices. \ No newline at end of file diff --git a/frontend/layout/tests/model.ts b/frontend/layout/tests/model.ts index 01b043fd4b..fd67c9fae2 100644 --- a/frontend/layout/tests/model.ts +++ b/frontend/layout/tests/model.ts @@ -7,5 +7,6 @@ export function newLayoutTreeState(rootNode: LayoutNode): LayoutTreeState { return { rootNode, generation: 0, + pendingBackendActions: [], }; } diff --git a/package.json b/package.json index b9ff6e2c44..64409412d4 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,7 @@ }, "packageManager": "yarn@4.6.0", "workspaces": [ - "docs" + "docs", + "waveapps/*" ] } diff --git a/waveapps/jwt/describe.json b/waveapps/jwt/describe.json new file mode 100644 index 0000000000..1241dd7270 --- /dev/null +++ b/waveapps/jwt/describe.json @@ -0,0 +1,50 @@ +{ + "name": "JWT Decoder", + "version": "1.0.0", + "baseurl": "/", + "description": "A simple JWT token decoder that parses and displays JWT header and payload information", + "actions": [ + { + "name": "decode", + "method": "POST", + "path": "/api/decode", + "description": "Decode a JWT token and return header and payload", + "inputschema": "DecodeRequest", + "outputschema": "DecodeResponse" + } + ], + "schemas": { + "config": { + "type": "object", + "description": "Configuration for JWT decoder (currently empty)", + "properties": {} + }, + "data": { + "type": "object", + "description": "Last decoded JWT token data or error", + "properties": { + "header": { "type": "object", "description": "JWT header" }, + "payload": { "type": "object", "description": "JWT payload" }, + "signature": { "type": "string", "description": "JWT signature (base64url encoded)" }, + "error": { "type": "string", "description": "Error message if decoding failed" } + } + }, + "DecodeRequest": { + "type": "object", + "description": "Request to decode a JWT token", + "properties": { + "token": { "type": "string", "description": "JWT token to decode" } + }, + "required": ["token"] + }, + "DecodeResponse": { + "type": "object", + "description": "Decoded JWT token response", + "properties": { + "header": { "type": "object", "description": "JWT header" }, + "payload": { "type": "object", "description": "JWT payload" }, + "signature": { "type": "string", "description": "JWT signature (base64url encoded)" } + } + } + } +} diff --git a/waveapps/jwt/index.html b/waveapps/jwt/index.html new file mode 100644 index 0000000000..6979e65070 --- /dev/null +++ b/waveapps/jwt/index.html @@ -0,0 +1,12 @@ + + + + + + JWT Decoder + + +
+ + + \ No newline at end of file diff --git a/waveapps/jwt/package.json b/waveapps/jwt/package.json new file mode 100644 index 0000000000..fe13f454d9 --- /dev/null +++ b/waveapps/jwt/package.json @@ -0,0 +1,39 @@ +{ + "name": "jwt-decoder", + "version": "1.0.0", + "description": "Simple JWT decoder waveapp", + "type": "module", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "concurrently \"node server.js\" \"vite\"", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@hono/node-server": "^1.19.0", + "hono": "^4.0.0", + "jsonwebtoken": "^9.0.2", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.12", + "@types/jsonwebtoken": "^9", + "@types/node": "^20.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^5.0.0", + "concurrently": "^8.2.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^7.0.0" + }, + "keywords": [ + "jwt", + "decoder", + "waveapp" + ], + "author": "Wave Terminal", + "license": "Apache-2.0" +} diff --git a/waveapps/jwt/server.js b/waveapps/jwt/server.js new file mode 100644 index 0000000000..d0cd9c0f1d --- /dev/null +++ b/waveapps/jwt/server.js @@ -0,0 +1,108 @@ +import { serve } from "@hono/node-server"; +import { serveStatic } from "@hono/node-server/serve-static"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import jwt from "jsonwebtoken"; + +const app = new Hono(); + +// Helper function to send messages to parent process +function sendWaveAppMessage(message) { + if (process.send) { + process.send(message); + } else { + console.log(`#waveapp${JSON.stringify(message)}`); + } +} + +app.use("/*", cors()); +app.use("/static/*", serveStatic({ root: "./dist" })); + +// State management +let appData = null; +let appConfig = {}; + +// Waveapp spec handlers +app.get("/api/config", (c) => { + return c.json(appConfig); +}); + +app.put("/api/config", async (c) => { + try { + const newConfig = await c.req.json(); + appConfig = { ...appConfig, ...newConfig }; + return c.json(appConfig); + } catch (error) { + return c.json({ error: "Invalid config format" }, 400); + } +}); + +app.get("/api/data", (c) => { + return c.json(appData || {}); +}); + +app.get("/api/describe", serveStatic({ path: "./describe.json" })); + +// JWT decode endpoint +app.post("/api/decode", async (c) => { + try { + const { token } = await c.req.json(); + + if (!token) { + const errorData = { error: "Token is required" }; + appData = errorData; + return c.json(errorData, 400); + } + + // Try to verify to get detailed parsing errors from the library + try { + jwt.verify(token, "dummy-secret", { ignoreExpiration: true, ignoreNotBefore: true }); + } catch (verifyError) { + // If it's a signature error, that's expected - continue to decode + if (verifyError.name === "JsonWebTokenError" && verifyError.message === "invalid signature") { + // Token is structurally valid, just can't verify signature + } else { + // This is a real parsing/format error from the library + const errorData = { error: verifyError.message }; + appData = errorData; + return c.json(errorData, 400); + } + } + + // Decode JWT since we know it's structurally valid + const decoded = jwt.decode(token, { complete: true }); + + const result = { + header: decoded.header, + payload: decoded.payload, + signature: decoded.signature, + }; + + // Store the decoded data + appData = result; + + return c.json(result); + } catch (error) { + const errorData = { error: error.message || "Failed to decode JWT token" }; + appData = errorData; + return c.json(errorData, 400); + } +}); + +// Serve static files from dist directory (built by Vite) +app.use("/*", serveStatic({ root: "./dist" })); + +const server = serve({ + fetch: app.fetch, + port: 3000, // Fixed port to match Vite proxy config +}); + +server.on("listening", () => { + const port = server.address().port; + console.log(`JWT Decoder server running on port ${port}`); + + sendWaveAppMessage({ + type: "listening", + port: port, + }); +}); diff --git a/waveapps/jwt/src/index.css b/waveapps/jwt/src/index.css new file mode 100644 index 0000000000..e718370f93 --- /dev/null +++ b/waveapps/jwt/src/index.css @@ -0,0 +1,62 @@ +/* Copyright 2025, Command Line Inc. + SPDX-License-Identifier: Apache-2.0 */ + +@import "https://github.com/tailwindcss"; + +@theme { + --color-background: rgb(34, 34, 34); + --color-foreground: #f7f7f7; + --color-white: #f7f7f7; + --color-muted-foreground: rgb(195, 200, 194); + --color-secondary: rgb(195, 200, 194); + --color-accent-50: rgb(236, 253, 232); + --color-accent-100: rgb(209, 250, 202); + --color-accent-200: rgb(167, 243, 168); + --color-accent-300: rgb(110, 231, 133); + --color-accent-400: rgb(88, 193, 66); /* main accent color */ + --color-accent-500: rgb(63, 162, 51); + --color-accent-600: rgb(47, 133, 47); + --color-accent-700: rgb(34, 104, 43); + --color-accent-800: rgb(22, 81, 35); + --color-accent-900: rgb(15, 61, 29); + --color-error: rgb(229, 77, 46); + --color-warning: rgb(224, 185, 86); + --color-success: rgb(78, 154, 6); + --color-panel: rgba(31, 33, 31, 0.5); + --color-hover: rgba(255, 255, 255, 0.1); + --color-border: rgba(255, 255, 255, 0.16); + --color-modalbg: #232323; + --color-accentbg: rgba(88, 193, 66, 0.5); + --color-hoverbg: rgba(255, 255, 255, 0.2); + --color-accent: rgb(88, 193, 66); + --color-accenthover: rgb(118, 223, 96); + + --font-sans: "Inter", sans-serif; + --font-mono: "Hack", monospace; + --font-markdown: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji"; + + --text-xxs: 10px; + --text-title: 18px; + --text-default: 14px; + + --radius: 8px; + + /* ANSI Colors (Default Dark Palette) */ + --ansi-black: #757575; + --ansi-red: #cc685c; + --ansi-green: #76c266; + --ansi-yellow: #cbca9b; + --ansi-blue: #85aacb; + --ansi-magenta: #cc72ca; + --ansi-cyan: #74a7cb; + --ansi-white: #c1c1c1; + --ansi-brightblack: #727272; + --ansi-brightred: #cc9d97; + --ansi-brightgreen: #a3dd97; + --ansi-brightyellow: #cbcaaa; + --ansi-brightblue: #9ab6cb; + --ansi-brightmagenta: #cc8ecb; + --ansi-brightcyan: #b7b8cb; + --ansi-brightwhite: #f0f0f0; +} \ No newline at end of file diff --git a/waveapps/jwt/src/main.tsx b/waveapps/jwt/src/main.tsx new file mode 100644 index 0000000000..dd19ff1d0a --- /dev/null +++ b/waveapps/jwt/src/main.tsx @@ -0,0 +1,122 @@ +import React, { useState } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +interface DecodedJWT { + header: Record; + payload: Record; + signature: string; +} + +const App: React.FC = () => { + const [token, setToken] = useState(''); + const [decoded, setDecoded] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const decodeToken = async () => { + if (!token.trim()) { + setError('Please enter a JWT token'); + setDecoded(null); + return; + } + + setLoading(true); + setError(null); + + try { + const response = await fetch('https://github.com/api/decode', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token }) + }); + + const result = await response.json(); + + if (!response.ok) { + setError(result.error || `Server error: ${response.status} ${response.statusText}`); + setDecoded(null); + return; + } + + setDecoded(result); + setError(null); + } catch (err) { + if (err instanceof TypeError && err.message.includes('fetch')) { + setError('Network error: Unable to connect to server'); + } else if (err instanceof SyntaxError) { + setError('Server response error: Invalid JSON received'); + } else { + const errorMessage = err instanceof Error ? err.message : 'Unknown error'; + setError(`Failed to decode token: ${errorMessage}`); + } + setDecoded(null); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

JWT Decoder

+ +
+ {/* Input Section */} +
+
+ +