morphbox
Version:
Docker-based AI sandbox for development with Claude integration
836 lines (824 loc) • 25.7 kB
JavaScript
import { g as get_store_value, h as set_current_component, j as current_component, r as run_all } from './ssr-Bi8A3Ffq.js';
import { d as derived, w as writable } from './index-6zo8caE3.js';
const dirty_components = [];
const binding_callbacks = [];
let render_callbacks = [];
const flush_callbacks = [];
const resolved_promise = /* @__PURE__ */ Promise.resolve();
let update_scheduled = false;
function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
function tick() {
schedule_update();
return resolved_promise;
}
function add_render_callback(fn) {
render_callbacks.push(fn);
}
const seen_callbacks = /* @__PURE__ */ new Set();
let flushidx = 0;
function flush() {
if (flushidx !== 0) {
return;
}
const saved_component = current_component;
do {
try {
while (flushidx < dirty_components.length) {
const component = dirty_components[flushidx];
flushidx++;
set_current_component(component);
update(component.$$);
}
} catch (e) {
dirty_components.length = 0;
flushidx = 0;
throw e;
}
set_current_component(null);
dirty_components.length = 0;
flushidx = 0;
while (binding_callbacks.length) binding_callbacks.pop()();
for (let i = 0; i < render_callbacks.length; i += 1) {
const callback = render_callbacks[i];
if (!seen_callbacks.has(callback)) {
seen_callbacks.add(callback);
callback();
}
}
render_callbacks.length = 0;
} while (dirty_components.length);
while (flush_callbacks.length) {
flush_callbacks.pop()();
}
update_scheduled = false;
seen_callbacks.clear();
set_current_component(saved_component);
}
function update($$) {
if ($$.fragment !== null) {
$$.update();
run_all($$.before_update);
const dirty = $$.dirty;
$$.dirty = [-1];
$$.fragment && $$.fragment.p($$.ctx, dirty);
$$.after_update.forEach(add_render_callback);
}
}
const defaultSettings = {
theme: "dark",
customTheme: {
background: "#1e1e1e",
foreground: "#d4d4d4",
accent: "#007acc"
},
terminal: {
fontSize: 12,
fontFamily: '"Cascadia Code", "Fira Code", monospace',
lineHeight: 1.1,
cursorStyle: "block",
cursorBlink: true
},
panels: {
defaultPositions: {
terminal: { x: 0, y: 40, width: "100%", height: "calc(100% - 62px)" },
settings: { x: 50, y: 50, width: "600px", height: "500px" }
},
snapToGrid: true,
gridSize: 10
},
shortcuts: {
"toggle-terminal": "Ctrl+`",
"toggle-settings": "Ctrl+,",
"clear-terminal": "Ctrl+L",
"new-terminal": "Ctrl+Shift+`",
"close-panel": "Escape",
"save-settings": "Ctrl+S"
},
editor: {
fontSize: 14,
fontFamily: '"Cascadia Code", "Fira Code", monospace',
lineHeight: 1.5,
wordWrap: false,
minimap: true,
theme: "vs-dark"
},
customPanels: {
systemPrompt: `Create a vanilla JavaScript panel for MorphBox with the following requirements:
Panel Name: {name}
Description: {description}
Generate a complete HTML/CSS/JavaScript panel that:
1. Uses vanilla JavaScript (no frameworks)
2. Has access to these variables: panelId, data, websocketUrl
3. Can connect to WebSocket for real-time data: const ws = new WebSocket(websocketUrl)
4. Uses MorphBox CSS variables for theming (--bg-primary, --text-primary, --border-color, etc.)
5. Implements the functionality described above
6. Uses proper error handling and loading states where applicable
7. IMPORTANT: The JavaScript code will be automatically wrapped in an onMount() function, so you can safely access DOM elements directly without waiting for DOMContentLoaded
8. Is responsive and works well on mobile
WebSocket Access:
- Connect using: new WebSocket(websocketUrl)
- Listen for 'OUTPUT' events (terminal output)
- Listen for 'context_update' events (Claude Code context tracking)
- See full API: https://github.com/instant-unicorn/morphbox/blob/main/docs/CUSTOM_PANELS.md
IMPORTANT: Return ONLY the HTML code starting with <div> tags. Do not include any markdown formatting, code blocks, or explanations. Just the raw HTML/CSS/JavaScript code.
The panel should follow this structure:
<!--
@morphbox-panel
id: (will be generated)
name: {name}
description: {description}
version: 1.0.0
-->
<div class="custom-panel">
<div class="panel-header">
<h2>{name}</h2>
</div>
<div class="panel-content">
<!-- Panel content here -->
</div>
</div>
<style>
/* Panel styles using CSS variables */
</style>
<script>
// Panel logic here
// Available: panelId, data, websocketUrl
// Use vanilla JS, no frameworks
<\/script>
Make it fully functional and production-ready. Use modern JavaScript features.`
}
};
function createSettingsStore() {
const { subscribe, set, update: update2 } = writable(defaultSettings);
return {
subscribe,
load: () => {
if (typeof window !== "undefined") {
const saved = localStorage.getItem("morphbox-settings");
if (saved) {
try {
const parsed = JSON.parse(saved);
set({ ...defaultSettings, ...parsed });
} catch (e) {
console.error("Failed to load settings:", e);
set(defaultSettings);
}
}
}
},
save: () => {
const current = get_store_value(settings);
if (typeof window !== "undefined") {
localStorage.setItem("morphbox-settings", JSON.stringify(current));
}
},
reset: () => {
set(defaultSettings);
if (typeof window !== "undefined") {
localStorage.removeItem("morphbox-settings");
}
},
update: update2
};
}
const settings = createSettingsStore();
const defaultPanelConfigs = {
terminal: {
type: "terminal",
title: "Terminal",
size: { width: 800, height: 400 },
persistent: false,
terminalPersistent: true
},
claude: {
type: "claude",
title: "Claude",
size: { width: 800, height: 400 },
persistent: true,
terminalPersistent: true
},
fileExplorer: {
type: "fileExplorer",
title: "File Explorer",
size: { width: 300, height: 600 },
persistent: false
},
editor: {
type: "editor",
title: "Editor",
size: { width: 800, height: 600 },
persistent: false
},
codeEditor: {
type: "codeEditor",
title: "Code Editor",
size: { width: 800, height: 600 },
persistent: false
},
preview: {
type: "preview",
title: "Preview",
size: { width: 600, height: 500 },
persistent: false
},
settings: {
type: "settings",
title: "Settings",
size: { width: 600, height: 400 },
persistent: false
}
};
function restoreWebSocketConnection(panelId, connectionId) {
return null;
}
function createPanelSnapshot(state) {
try {
const snapshot = {
panels: state.panels.map((panel) => ({
...panel,
websocketConnections: void 0
// Remove non-serializable data
})),
workspaces: state.workspaces,
activeWorkspaceId: state.activeWorkspaceId,
layout: state.layout,
activePanel: state.activePanel,
timestamp: Date.now()
};
return JSON.stringify(snapshot);
} catch (error) {
console.error("Failed to create panel snapshot:", error);
return "";
}
}
function restorePanelSnapshot(snapshotData) {
try {
const snapshot = JSON.parse(snapshotData);
const defaultWorkspace = {
id: "workspace-1",
name: "Workspace 1",
created: Date.now()
};
return {
panels: snapshot.panels.map((panel) => ({
...panel,
workspaceId: panel.workspaceId || defaultWorkspace.id,
websocketConnections: /* @__PURE__ */ new Map()
})),
workspaces: snapshot.workspaces || [defaultWorkspace],
activeWorkspaceId: snapshot.activeWorkspaceId || defaultWorkspace.id,
layout: snapshot.layout || "floating",
activePanel: snapshot.activePanel || null
};
} catch (error) {
console.error("Failed to restore panel snapshot:", error);
return null;
}
}
function createPanelStore() {
const defaultWorkspace = {
id: "workspace-1",
name: "Workspace 1",
created: Date.now()
};
const initialState = {
panels: [],
workspaces: [defaultWorkspace],
activeWorkspaceId: defaultWorkspace.id,
layout: "grid",
// Changed to grid for new layout
activePanel: null
};
const { subscribe, set, update: update2 } = writable(initialState);
const generateId = () => `panel-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const getNextPosition = (panels2) => {
const baseX = 50;
const baseY = 50;
const offset = 30;
const count = panels2.length;
return {
x: baseX + count * offset % 300,
y: baseY + count * offset % 200
};
};
const getHighestZIndex = (panels2) => {
return Math.max(0, ...panels2.map((p) => p.zIndex || 0));
};
return {
subscribe,
// Add a new panel
addPanel: (type, config) => {
update2((state) => {
const defaultConfig = defaultPanelConfigs[type] || {};
const isCustomPanel = type.match(/^[a-z]/) || type.includes("-");
const id = isCustomPanel ? type : generateId();
const position = config?.position || getNextPosition(state.panels);
const zIndex = getHighestZIndex(state.panels) + 1;
const { id: configId, ...configWithoutId } = config || {};
const currentSettings = get_store_value(settings);
const defaultColors = currentSettings.panels?.defaultPanelColors || {};
const newPanel = {
id,
type,
title: "Untitled",
workspaceId: configWithoutId?.workspaceId || state.activeWorkspaceId,
position,
size: { width: 400, height: 300 },
persistent: false,
zIndex,
headerColor: defaultColors?.headerColor,
backgroundColor: defaultColors?.backgroundColor,
borderColor: defaultColors?.borderColor,
...defaultConfig,
...configWithoutId
};
return {
...state,
panels: [...state.panels, newPanel],
activePanel: id
};
});
},
// Remove a panel
removePanel: (id) => {
update2((state) => ({
...state,
panels: state.panels.filter((p) => p.id !== id),
activePanel: state.activePanel === id ? null : state.activePanel
}));
},
// Update a panel
updatePanel: (id, updates) => {
update2((state) => ({
...state,
panels: state.panels.map(
(p) => p.id === id ? { ...p, ...updates } : p
)
}));
},
// Set active panel (bring to front)
setActivePanel: (id) => {
update2((state) => {
if (!id) return { ...state, activePanel: null };
const highestZ = getHighestZIndex(state.panels);
const panels2 = state.panels.map(
(p) => p.id === id ? { ...p, zIndex: highestZ + 1 } : p
);
return {
...state,
panels: panels2,
activePanel: id
};
});
},
// Toggle minimize
toggleMinimize: (id) => {
update2((state) => ({
...state,
panels: state.panels.map(
(p) => p.id === id ? { ...p, minimized: !p.minimized, maximized: false } : p
)
}));
},
// Toggle maximize
toggleMaximize: (id) => {
update2((state) => ({
...state,
panels: state.panels.map(
(p) => p.id === id ? { ...p, maximized: !p.maximized, minimized: false } : p
)
}));
},
// Set layout mode
setLayout: (layout) => {
update2((state) => ({ ...state, layout }));
},
// Clear all panels
clearPanels: () => {
update2((state) => ({
...state,
panels: state.panels.filter((p) => p.persistent),
activePanel: null
}));
},
// Save configuration to localStorage
saveConfiguration: (name = "default") => {
const state = get_store_value({ subscribe });
const config = {
panels: state.panels,
layout: state.layout
};
localStorage.setItem(`panel-config-${name}`, JSON.stringify(config));
},
// Load configuration from localStorage
loadConfiguration: (name = "default") => {
const saved = localStorage.getItem(`panel-config-${name}`);
if (saved) {
try {
const config = JSON.parse(saved);
const defaultWorkspace2 = {
id: "workspace-1",
name: "Workspace 1",
created: Date.now()
};
set({
panels: (config.panels || []).map((panel) => ({
...panel,
workspaceId: panel.workspaceId || defaultWorkspace2.id
})),
workspaces: config.workspaces || [defaultWorkspace2],
activeWorkspaceId: config.activeWorkspaceId || defaultWorkspace2.id,
layout: config.layout || "floating",
activePanel: null
});
return true;
} catch (e) {
console.error("Failed to load panel configuration:", e);
return false;
}
}
return false;
},
// Get all saved configurations
getSavedConfigurations: () => {
const configs = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key?.startsWith("panel-config-")) {
configs.push(key.replace("panel-config-", ""));
}
}
return configs;
},
// Delete a saved configuration
deleteConfiguration: (name) => {
localStorage.removeItem(`panel-config-${name}`);
},
// Initialize with default panels
initializeDefaults: () => {
update2((state) => {
if (state.panels.length > 0) return state;
const defaultPanels = [
{
...defaultPanelConfigs.claude,
id: generateId(),
position: { x: 340, y: 440 }
}
];
return {
...state,
panels: defaultPanels,
activePanel: defaultPanels[0].id
};
});
},
// Hot reload recovery methods
recoverFromHotReload: () => {
},
// Terminal persistence methods
setTerminalPersistence: (id, persistent) => {
update2((state) => ({
...state,
panels: state.panels.map(
(p) => p.id === id ? { ...p, terminalPersistent: persistent } : p
)
}));
},
// WebSocket connection management
preserveWebSocket: (panelId, connectionId, websocket) => {
update2((state) => ({
...state,
panels: state.panels.map((p) => {
if (p.id === panelId) {
const connections = p.websocketConnections || /* @__PURE__ */ new Map();
connections.set(connectionId, websocket);
return { ...p, websocketConnections: connections };
}
return p;
})
}));
},
restoreWebSocket: (panelId, connectionId) => {
return restoreWebSocketConnection();
},
// State snapshot methods
createSnapshot: () => {
const state = get_store_value({ subscribe });
return createPanelSnapshot(state);
},
restoreSnapshot: (snapshotData) => {
const restoredState2 = restorePanelSnapshot(snapshotData);
if (restoredState2) {
set(restoredState2);
return true;
}
return false;
},
// Manual state preservation
saveState: () => {
get_store_value({ subscribe });
},
// Clear all state
clearState: () => {
},
// Workspace management
addWorkspace: (name) => {
update2((state) => {
const id = `workspace-${Date.now()}`;
const workspaceName = name || `Workspace ${state.workspaces.length + 1}`;
const newWorkspace = {
id,
name: workspaceName,
created: Date.now()
};
return {
...state,
workspaces: [...state.workspaces, newWorkspace],
activeWorkspaceId: id
};
});
},
removeWorkspace: (workspaceId) => {
update2((state) => {
if (state.workspaces.length <= 1) return state;
const newWorkspaces = state.workspaces.filter((w) => w.id !== workspaceId);
const newPanels = state.panels.filter((p) => p.workspaceId !== workspaceId);
const newActiveWorkspaceId = workspaceId === state.activeWorkspaceId ? newWorkspaces[0].id : state.activeWorkspaceId;
return {
...state,
workspaces: newWorkspaces,
panels: newPanels,
activeWorkspaceId: newActiveWorkspaceId,
activePanel: null
};
});
},
switchWorkspace: (workspaceId) => {
update2((state) => ({
...state,
activeWorkspaceId: workspaceId,
activePanel: null
}));
},
renameWorkspace: (workspaceId, newName) => {
update2((state) => ({
...state,
workspaces: state.workspaces.map(
(w) => w.id === workspaceId ? { ...w, name: newName } : w
)
}));
},
// Clear all panels (for grid layout)
clear: () => {
update2((state) => ({
...state,
panels: state.panels.filter((p) => p.workspaceId !== state.activeWorkspaceId),
activePanel: null
}));
},
// Check if recovering from hot reload
isRecoveringFromHotReload: () => {
const state = get_store_value({ subscribe });
return state.hotReloadRecovery || false;
}
};
}
const panelStore = createPanelStore();
const panels = derived(
panelStore,
($store) => $store.panels.filter((p) => p.workspaceId === $store.activeWorkspaceId)
);
derived(panelStore, ($store) => $store.panels);
derived(panelStore, ($store) => $store.workspaces || []);
derived(panelStore, ($store) => $store.activeWorkspaceId);
derived(
panelStore,
($store) => $store.workspaces.find((w) => w.id === $store.activeWorkspaceId) ?? null
);
const activePanel = derived(
panelStore,
($store) => $store.panels.find((p) => p.id === $store.activePanel) ?? null
);
derived(panelStore, ($store) => {
const byType = {};
$store.panels.filter((p) => p.workspaceId === $store.activeWorkspaceId).forEach((panel) => {
if (!byType[panel.type]) byType[panel.type] = [];
byType[panel.type].push(panel);
});
return byType;
});
derived(
panelStore,
($store) => $store.panels.filter((p) => p.workspaceId === $store.activeWorkspaceId).filter((p) => !p.minimized)
);
function createPanelRegistry() {
const { subscribe, set, update: update2 } = writable({
panels: /* @__PURE__ */ new Map()
});
return {
subscribe,
// Register a new panel
register(definition) {
update2((state) => {
state.panels.set(definition.id, definition);
return state;
});
this.saveRegistry();
},
// Unregister a panel
unregister(id) {
update2((state) => {
state.panels.delete(id);
return state;
});
this.saveRegistry();
},
// Get a panel definition
get(id) {
const state = get_store_value({ subscribe });
return state.panels.get(id);
},
// Get all panels
getAll() {
const state = get_store_value({ subscribe });
return Array.from(state.panels.values());
},
// Get panels by feature
getByFeature(feature) {
const state = get_store_value({ subscribe });
return Array.from(state.panels.values()).filter((panel) => panel.features.includes(feature));
},
// Load a panel component dynamically
async loadComponent(id) {
const panel = this.get(id);
if (!panel) return null;
try {
const module = await import(
/* @vite-ignore */
panel.path
);
return module.default;
} catch (error) {
console.error(`Failed to load panel component: ${id}`, error);
return null;
}
},
// Save registry to localStorage
saveRegistry() {
const state = get_store_value({ subscribe });
const customPanelsOnly = Array.from(state.panels.entries()).filter(([id, panel]) => panel.isCustom).map(([id, panel]) => ({
...panel,
component: null,
// Don't serialize components
createdAt: panel.createdAt.toISOString()
}));
localStorage.setItem("panel-registry", JSON.stringify(customPanelsOnly));
},
// Load registry from localStorage
loadRegistry() {
const saved = localStorage.getItem("panel-registry");
if (!saved) return;
try {
const parsed = JSON.parse(saved);
const panels2 = /* @__PURE__ */ new Map();
for (const panel of parsed) {
if (panel.isCustom) {
panels2.set(panel.id, {
...panel,
createdAt: new Date(panel.createdAt),
component: null
// Will be loaded on demand
});
}
}
update2((state) => {
for (const [id, panel] of panels2) {
state.panels.set(id, panel);
}
return state;
});
} catch (error) {
console.error("Failed to load panel registry:", error);
localStorage.removeItem("panel-registry");
}
},
// Initialize with built-in panels
initializeBuiltins() {
const builtinPanels2 = [
{
id: "terminal",
name: "Terminal",
description: "Interactive terminal emulator",
path: "$lib/Terminal.svelte",
features: ["terminal", "websocket"],
createdAt: /* @__PURE__ */ new Date(),
isCustom: false
},
{
id: "claude",
name: "Claude",
description: "Claude AI assistant",
path: "$lib/Claude.svelte",
features: ["terminal", "websocket", "ai"],
createdAt: /* @__PURE__ */ new Date(),
isCustom: false
},
{
id: "fileExplorer",
name: "File Explorer",
description: "Browse and manage files",
path: "$lib/panels/FileExplorer/FileExplorer.svelte",
features: ["fileSystem", "stateManagement"],
createdAt: /* @__PURE__ */ new Date(),
isCustom: false
},
{
id: "codeEditor",
name: "Code Editor",
description: "Edit code with syntax highlighting",
path: "$lib/panels/CodeEditor/CodeEditor.svelte",
features: ["fileSystem", "stateManagement"],
createdAt: /* @__PURE__ */ new Date(),
isCustom: false
},
{
id: "settings",
name: "Settings",
description: "Application settings and preferences",
path: "$lib/panels/Settings/Settings.svelte",
features: ["formHandling", "stateManagement"],
createdAt: /* @__PURE__ */ new Date(),
isCustom: false
},
{
id: "promptQueue",
name: "Prompt Queue",
description: "Queue and manage prompts for Claude",
path: "$lib/panels/PromptQueue/PromptQueue.svelte",
features: ["ai", "queue", "automation"],
createdAt: /* @__PURE__ */ new Date(),
isCustom: false
},
{
id: "webBrowser",
name: "Web Browser",
description: "Preview web pages and local servers",
path: "$lib/panels/WebBrowser/WebBrowser.svelte",
features: ["preview", "browser", "development"],
createdAt: /* @__PURE__ */ new Date(),
isCustom: false
},
{
id: "gitPanel",
name: "Git",
description: "Git version control management",
path: "$lib/panels/GitPanel/GitPanel.svelte",
features: ["git", "vcs", "development"],
createdAt: /* @__PURE__ */ new Date(),
isCustom: false
},
{
id: "taskRunner",
name: "Task Runner",
description: "Run npm scripts and custom commands",
path: "$lib/panels/TaskRunner/TaskRunner.svelte",
features: ["tasks", "terminal", "development"],
createdAt: /* @__PURE__ */ new Date(),
isCustom: false
}
];
for (const panel of builtinPanels2) {
this.register({ ...panel, component: null });
}
}
};
}
const panelRegistry = createPanelRegistry();
const customPanels = derived(
panelRegistry,
($registry) => Array.from($registry.panels.values()).filter((p) => p.isCustom)
);
const builtinPanels = derived(
panelRegistry,
($registry) => Array.from($registry.panels.values()).filter((p) => !p.isCustom)
);
if (typeof window !== "undefined") {
console.log("[PanelRegistry] Initializing...");
panelRegistry.loadRegistry();
console.log("[PanelRegistry] Initializing built-in panels...");
panelRegistry.initializeBuiltins();
const registry = get_store_value(panelRegistry);
console.log("[PanelRegistry] Total panels:", registry.panels.size);
console.log("[PanelRegistry] All panels:", Array.from(registry.panels.keys()));
const builtins = get_store_value(builtinPanels);
const customs = get_store_value(customPanels);
console.log("[PanelRegistry] Built-in panels:", builtins.map((p) => p.id));
console.log("[PanelRegistry] Custom panels:", customs.map((p) => p.id));
}
export { panels as a, activePanel as b, customPanels as c, builtinPanels as d, panelRegistry as e, panelStore as p, settings as s, tick as t };
//# sourceMappingURL=registry-kTtxA5oL.js.map