MorphBox panels are modular UI components built with SvelteKit that provide different functionalities within the MorphBox environment. Each panel runs in an isolated Docker container and communicates via WebSocket connections.
- BasePanel.svelte: Base component all panels extend from (
web/src/lib/panels/BasePanel.svelte) - Panel Registry: Central registry managing all available panels (
web/src/lib/panels/registry.ts) - Panel Types: TypeScript interfaces defining panel structure (
web/src/lib/panels/types.ts) - Custom Panel Loader: Dynamic panel compilation system (
web/src/lib/panels/CustomPanelLoader.svelte)
interface PanelConfig {
id: string; // Unique panel identifier
title: string; // Display title
icon?: string; // Optional icon
component: ComponentType; // Svelte component
defaultWidth?: number; // Default dimensions
defaultHeight?: number;
minWidth?: number; // Size constraints
minHeight?: number;
maxWidth?: number;
maxHeight?: number;
resizable?: boolean; // UI capabilities
movable?: boolean;
closable?: boolean;
minimizable?: boolean;
maximizable?: boolean;
}
interface PanelState {
id: string;
x: number; // Position
y: number;
width: number; // Current size
height: number;
isMinimized: boolean; // UI state
isMaximized: boolean;
zIndex: number; // Stacking order
}Create a new file in web/src/lib/panels/YourPanel/YourPanel.svelte:
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { getContext } from 'svelte';
// Panel props passed from parent
export let panelId: string = '';
export let data: any = {};
// Access WebSocket if needed
const ws = getContext('websocket');
// Panel state
let isLoading = false;
let error: string | null = null;
onMount(() => {
console.log(`Panel ${panelId} mounted`);
// Initialize panel
});
onDestroy(() => {
console.log(`Panel ${panelId} destroyed`);
// Cleanup
});
</script>
<div class="your-panel">
<h2>Your Panel</h2>
{#if isLoading}
<p>Loading...</p>
{:else if error}
<p class="error">{error}</p>
{:else}
<!-- Your panel content here -->
{/if}
</div>
<style>
.your-panel {
padding: 1rem;
height: 100%;
overflow: auto;
}
.error {
color: var(--error-color, #f44336);
}
</style>Add to web/src/lib/panels/registry.ts in the initializeBuiltins() method:
{
id: 'yourPanel',
name: 'Your Panel',
description: 'Description of your panel',
path: '$lib/panels/YourPanel/YourPanel.svelte',
features: ['feature1', 'feature2'], // Tags for categorization
createdAt: new Date(),
isCustom: false // false for built-in, true for user-created
}Update web/src/lib/panels/index.ts:
export { default as YourPanel } from './YourPanel/YourPanel.svelte';Panels can communicate with the backend via WebSocket:
<script lang="ts">
import { getContext } from 'svelte';
const ws = getContext('websocket');
function sendMessage(data: any) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'panel_message',
panelId,
data
}));
}
}
// Listen for messages
$: if (ws) {
ws.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
if (message.panelId === panelId) {
// Handle message for this panel
}
});
}
</script>Panels can interact with the container's file system:
<script lang="ts">
async function readFile(path: string) {
const response = await fetch(`/api/files/read`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path })
});
return await response.text();
}
async function writeFile(path: string, content: string) {
await fetch(`/api/files/write`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path, content })
});
}
</script>Panels can execute commands in the container:
<script lang="ts">
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
let terminal: Terminal;
let terminalElement: HTMLDivElement;
onMount(() => {
terminal = new Terminal({
theme: {
background: '#1e1e1e',
foreground: '#d4d4d4'
}
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(terminalElement);
fitAddon.fit();
// Connect to PTY via WebSocket
ws.send(JSON.stringify({
type: 'create_pty',
panelId
}));
terminal.onData((data) => {
ws.send(JSON.stringify({
type: 'pty_input',
panelId,
data
}));
});
});
</script>
<div bind:this={terminalElement} class="terminal-container"></div>Panels can use Svelte stores for state management:
<script lang="ts">
import { writable, derived } from 'svelte/store';
// Local state store
const items = writable<Item[]>([]);
const selectedItem = writable<Item | null>(null);
// Derived stores
const itemCount = derived(items, $items => $items.length);
const hasSelection = derived(selectedItem, $item => $item !== null);
// Persist state to localStorage
items.subscribe(value => {
localStorage.setItem(`panel-${panelId}-items`, JSON.stringify(value));
});
onMount(() => {
const saved = localStorage.getItem(`panel-${panelId}-items`);
if (saved) {
items.set(JSON.parse(saved));
}
});
</script>- Location:
web/src/lib/Terminal.svelte(withsandboxed={true}) - Features: Full terminal emulator running in Docker container, safe for AI autonomy with
--dangerously-skip-permissionsmode - WebSocket Events:
create_session,terminal_input,terminal_output - Use Case: Primary terminal for running AI agents and automated tasks with contained blast radius
- Location:
web/src/lib/Terminal.svelte(withsandboxed={false}) - Features: Full terminal emulator with direct host system access
- WebSocket Events:
create_session,terminal_input,terminal_output - Use Case: System administration tasks requiring host access (git push, NGINX builds, production deployments)
- Warning:
⚠️ Use with caution - has full host system access
- Location:
web/src/lib/panels/FileExplorer/FileExplorer.svelte - Features: File tree navigation, context menus, drag-and-drop
- State: Current directory, selected files, expanded folders
- Location:
web/src/lib/panels/CodeEditor/CodeEditor.svelte - Features: Monaco editor integration, syntax highlighting, tabs
- Dependencies:
@monaco-editor/loader,monaco-editor
- Location:
web/src/lib/panels/GitPanel/GitPanel.svelte - Features: Git status, diff viewer, commit interface
- Commands: Uses git CLI commands via WebSocket
- Location:
web/src/lib/panels/WebBrowser/WebBrowser.svelte - Features: iframe-based preview, URL navigation, refresh
- Security: Sandboxed iframe with restricted permissions
- Location:
web/src/lib/panels/Settings/Settings.svelte - Features: Theme selection, preferences, keybindings
- Storage: LocalStorage for client-side settings
- Location:
web/src/lib/panels/TaskRunner/TaskRunner.svelte - Features: npm scripts, custom commands, output streaming
- Integration: Reads package.json scripts
- Location:
web/src/lib/panels/PromptQueue/PromptQueue.svelte - Features: Queue management, batch processing, history
- Integration: Direct Claude API integration
The CustomPanelLoader.svelte component can compile and load panels at runtime:
// Load a custom panel from source code
const panelSource = `
<script>
export let panelId = '';
export let data = {};
</script>
<div>
<h2>Dynamic Panel</h2>
<p>Panel ID: {panelId}</p>
</div>
`;
// Register and load the panel
panelRegistry.register({
id: 'dynamic-panel',
name: 'Dynamic Panel',
description: 'Loaded at runtime',
path: 'dynamic',
features: ['custom'],
createdAt: new Date(),
isCustom: true,
component: null // Will be compiled on demand
});Panels can be discovered from:
- Built-in registry (hardcoded in
registry.ts) - LocalStorage (custom panels)
- Remote sources (via API endpoints)
- File system (scanning panel directories)
Panels should use CSS variables for theming consistency:
/* Use these variables in your panel styles */
--panel-bg: #ffffff;
--panel-border: #e0e0e0;
--panel-radius: 8px;
--panel-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
--panel-header-bg: #f5f5f5;
--panel-title-color: #333333;
--panel-control-color: #666666;
--panel-control-hover-bg: rgba(0, 0, 0, 0.05);
--error-color: #f44336;
--success-color: #4caf50;
--warning-color: #ff9800;
--info-color: #2196f3;Panels emit and listen to these events:
close: Panel is being closedminimize: Panel is minimizedmaximize: Panel is maximizedrestore: Panel is restored from min/maxfocus: Panel gains focusresize: Panel is resizedmove: Panel is moved
Access global event bus:
<script>
import { getContext } from 'svelte';
const eventBus = getContext('eventBus');
// Emit global event
eventBus.emit('panel:action', { panelId, action: 'save' });
// Listen for global events
eventBus.on('global:refresh', () => {
// Refresh panel content
});
</script>- Lazy Loading: Load heavy dependencies only when needed
- Virtual Scrolling: For large lists, implement virtual scrolling
- Debouncing: Debounce expensive operations (search, resize)
- Memoization: Cache computed values with derived stores
<script>
import { onMount } from 'svelte';
let error: Error | null = null;
async function loadData() {
try {
// Your async operation
} catch (e) {
console.error(`Panel ${panelId} error:`, e);
error = e instanceof Error ? e : new Error(String(e));
// Report to error tracking
ws?.send(JSON.stringify({
type: 'panel_error',
panelId,
error: error.message,
stack: error.stack
}));
}
}
// Global error boundary
window.addEventListener('error', (event) => {
if (event.filename?.includes(panelId)) {
error = new Error(event.message);
}
});
</script>- ARIA Labels: Add proper ARIA labels to interactive elements
- Keyboard Navigation: Support Tab, Enter, Escape keys
- Focus Management: Manage focus properly when panel opens/closes
- Screen Reader Support: Use semantic HTML and ARIA attributes
// Panel test example
import { render, fireEvent } from '@testing-library/svelte';
import YourPanel from './YourPanel.svelte';
describe('YourPanel', () => {
it('renders correctly', () => {
const { getByText } = render(YourPanel, {
props: {
panelId: 'test-panel',
data: {}
}
});
expect(getByText('Your Panel')).toBeTruthy();
});
it('handles user interaction', async () => {
const { getByRole } = render(YourPanel);
const button = getByRole('button');
await fireEvent.click(button);
// Assert expected behavior
});
});Remember that panels run inside Docker containers:
- File paths are relative to container filesystem
- Network requests go through container networking
- Resource limits apply (CPU, memory)
- Persistence requires volume mounts
- Input Validation: Validate all user inputs
- XSS Prevention: Sanitize HTML content
- CORS: Handle cross-origin requests properly
- Secrets: Never expose sensitive data in panel code
- Sandboxing: Use iframe sandboxing for untrusted content
Panels should support multiple instances:
<script>
// Use panelId to namespace everything
const storeKey = `panel-${panelId}-state`;
const wsChannel = `panel:${panelId}`;
const elementId = `panel-element-${panelId}`;
</script>- Svelte DevTools: Browser extension for debugging
- Console Logging: Use structured logging
- WebSocket Inspector: Monitor WS messages in browser DevTools
- Source Maps: Enable for better debugging experience
- Panel Not Loading: Check registry registration and path
- WebSocket Errors: Verify connection and message format
- State Loss: Ensure proper persistence logic
- Memory Leaks: Clean up event listeners in onDestroy
- Style Conflicts: Use scoped styles or CSS modules
- Svelte Documentation: https://svelte.dev/docs
- SvelteKit Documentation: https://kit.svelte.dev/docs
- Monaco Editor: https://microsoft.github.io/monaco-editor/
- xterm.js: https://xtermjs.org/docs/
- MorphBox GitHub: https://github.com/instant-unicorn/morphbox