diff --git a/apps/studio/assets/midscene-icon.png b/apps/studio/assets/midscene-icon.png
new file mode 100644
index 0000000000..3780090a91
Binary files /dev/null and b/apps/studio/assets/midscene-icon.png differ
diff --git a/apps/studio/package.json b/apps/studio/package.json
new file mode 100644
index 0000000000..12d44238c8
--- /dev/null
+++ b/apps/studio/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "studio",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "build": "rsbuild build && node scripts/sync-static-assets.mjs",
+ "dev": "concurrently -k -n build,app \"rsbuild dev\" \"node scripts/wait-for-electron-build.mjs && node scripts/launch-electron-dev.mjs\"",
+ "preview": "rsbuild preview --environment renderer",
+ "start": "node scripts/sync-static-assets.mjs && electron dist/main/main.cjs",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "react": "18.3.1",
+ "react-dom": "18.3.1"
+ },
+ "devDependencies": {
+ "@rsbuild/core": "^1.6.15",
+ "@rsbuild/plugin-less": "^1.5.0",
+ "@rsbuild/plugin-react": "^1.4.1",
+ "@rsbuild/plugin-type-check": "^1.3.2",
+ "@tailwindcss/postcss": "4.1.11",
+ "@types/node": "^18.0.0",
+ "@types/react": "^18.3.1",
+ "@types/react-dom": "^18.3.1",
+ "concurrently": "^8.2.0",
+ "electron": "41.2.0",
+ "less": "^4.2.0",
+ "tailwindcss": "4.1.11",
+ "typescript": "^5.8.3",
+ "vitest": "3.0.5"
+ }
+}
diff --git a/apps/studio/postcss.config.mjs b/apps/studio/postcss.config.mjs
new file mode 100644
index 0000000000..60cf15487f
--- /dev/null
+++ b/apps/studio/postcss.config.mjs
@@ -0,0 +1,7 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {
+ preflight: false,
+ },
+ },
+};
diff --git a/apps/studio/rsbuild.config.ts b/apps/studio/rsbuild.config.ts
new file mode 100644
index 0000000000..bd52a410e5
--- /dev/null
+++ b/apps/studio/rsbuild.config.ts
@@ -0,0 +1,90 @@
+import { defineConfig } from '@rsbuild/core';
+import { pluginLess } from '@rsbuild/plugin-less';
+import { pluginReact } from '@rsbuild/plugin-react';
+import { pluginTypeCheck } from '@rsbuild/plugin-type-check';
+import { version as appVersion } from './package.json';
+import {
+ rendererDevHost,
+ rendererDevPort,
+} from './scripts/renderer-dev-config.mjs';
+
+export default defineConfig({
+ server: {
+ host: rendererDevHost,
+ port: rendererDevPort,
+ },
+ dev: {
+ writeToDisk: true,
+ },
+ plugins: [pluginReact(), pluginLess(), pluginTypeCheck()],
+ environments: {
+ renderer: {
+ html: {
+ title: 'Midscene Studio',
+ },
+ source: {
+ entry: {
+ index: './src/renderer/index.tsx',
+ },
+ define: {
+ __APP_VERSION__: JSON.stringify(appVersion),
+ },
+ },
+ output: {
+ target: 'web',
+ distPath: {
+ root: 'dist/renderer',
+ },
+ sourceMap: true,
+ },
+ },
+ main: {
+ tools: {
+ htmlPlugin: false,
+ },
+ source: {
+ entry: {
+ main: {
+ import: './src/main/index.ts',
+ html: false,
+ },
+ },
+ },
+ output: {
+ target: 'node',
+ distPath: {
+ root: 'dist/main',
+ },
+ filename: {
+ js: '[name].cjs',
+ },
+ externals: ['electron'],
+ sourceMap: true,
+ },
+ },
+ preload: {
+ tools: {
+ htmlPlugin: false,
+ },
+ source: {
+ entry: {
+ preload: {
+ import: './src/preload/index.ts',
+ html: false,
+ },
+ },
+ },
+ output: {
+ target: 'node',
+ distPath: {
+ root: 'dist/preload',
+ },
+ filename: {
+ js: '[name].cjs',
+ },
+ externals: ['electron'],
+ sourceMap: true,
+ },
+ },
+ },
+});
diff --git a/apps/studio/scripts/launch-electron-dev.mjs b/apps/studio/scripts/launch-electron-dev.mjs
new file mode 100644
index 0000000000..521d4337f6
--- /dev/null
+++ b/apps/studio/scripts/launch-electron-dev.mjs
@@ -0,0 +1,37 @@
+import { spawn } from 'node:child_process';
+import { createRequire } from 'node:module';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { rendererDevUrl } from './renderer-dev-config.mjs';
+
+// Spawns Electron with MIDSCENE_STUDIO_RENDERER_URL sourced from the shared
+// dev config, so the port is not duplicated in package.json scripts.
+const require = createRequire(import.meta.url);
+const electronBinary = require('electron');
+const rootDir = path.resolve(
+ path.dirname(fileURLToPath(import.meta.url)),
+ '..',
+);
+
+const child = spawn(
+ electronBinary,
+ [path.join(rootDir, 'dist/main/main.cjs')],
+ {
+ env: { ...process.env, MIDSCENE_STUDIO_RENDERER_URL: rendererDevUrl },
+ stdio: 'inherit',
+ },
+);
+
+const forwardSignal = (signal) => {
+ if (!child.killed) child.kill(signal);
+};
+process.on('SIGINT', forwardSignal);
+process.on('SIGTERM', forwardSignal);
+
+child.on('exit', (code, signal) => {
+ if (signal) {
+ process.kill(process.pid, signal);
+ return;
+ }
+ process.exit(code ?? 0);
+});
diff --git a/apps/studio/scripts/renderer-dev-config.mjs b/apps/studio/scripts/renderer-dev-config.mjs
new file mode 100644
index 0000000000..824acd0837
--- /dev/null
+++ b/apps/studio/scripts/renderer-dev-config.mjs
@@ -0,0 +1,8 @@
+// Single source of truth for the renderer dev server host/port.
+// Imported by rsbuild.config.ts (to bind the dev server), by
+// scripts/wait-for-electron-build.mjs (to probe readiness) and by
+// scripts/launch-electron-dev.mjs (to tell the main process where
+// to load from via MIDSCENE_STUDIO_RENDERER_URL).
+export const rendererDevHost = '127.0.0.1';
+export const rendererDevPort = 3210;
+export const rendererDevUrl = `http://${rendererDevHost}:${rendererDevPort}`;
diff --git a/apps/studio/scripts/sync-static-assets.mjs b/apps/studio/scripts/sync-static-assets.mjs
new file mode 100644
index 0000000000..8af0933573
--- /dev/null
+++ b/apps/studio/scripts/sync-static-assets.mjs
@@ -0,0 +1,35 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const rootDir = path.resolve(__dirname, '..');
+
+export const defaultSourceDir = path.join(rootDir, 'assets');
+export const defaultTargetDir = path.join(rootDir, 'dist/assets');
+
+/**
+ * Copy the shell's static assets into the build output, wiping any prior
+ * target contents first so files removed from source do not linger in dist.
+ * Throws if `sourceDir` does not exist — there is no meaningful fallback
+ * when the asset bundle is missing.
+ */
+export const syncStaticAssets = async ({
+ sourceDir = defaultSourceDir,
+ targetDir = defaultTargetDir,
+} = {}) => {
+ await fs.access(sourceDir);
+ await fs.rm(targetDir, { force: true, recursive: true });
+ await fs.mkdir(path.dirname(targetDir), { recursive: true });
+ await fs.cp(sourceDir, targetDir, { recursive: true });
+ return targetDir;
+};
+
+const isDirectInvocation =
+ process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
+
+if (isDirectInvocation) {
+ const targetDir = await syncStaticAssets();
+ console.log(`Synced Midscene Studio static assets to ${targetDir}`);
+}
diff --git a/apps/studio/scripts/wait-for-electron-build.mjs b/apps/studio/scripts/wait-for-electron-build.mjs
new file mode 100644
index 0000000000..25ac0b446a
--- /dev/null
+++ b/apps/studio/scripts/wait-for-electron-build.mjs
@@ -0,0 +1,122 @@
+import fs from 'node:fs';
+import http from 'node:http';
+import path from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { rendererDevUrl } from './renderer-dev-config.mjs';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const rootDir = path.resolve(__dirname, '..');
+
+export const defaultRequiredFiles = [
+ path.join(rootDir, 'dist/main/main.cjs'),
+ path.join(rootDir, 'dist/preload/preload.cjs'),
+];
+
+export const defaultRendererUrl = rendererDevUrl;
+export const defaultMaxWaitMs = 180000;
+export const defaultPollIntervalMs = 500;
+
+export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+
+export const readMtimeMs = (file) => {
+ try {
+ return fs.statSync(file).mtimeMs;
+ } catch (error) {
+ // Missing file is an expected signal ("not built yet"); anything else
+ // (permission denied, IO error, ...) should surface instead of being
+ // silently swallowed into a stale-build state.
+ if (error && error.code === 'ENOENT') return null;
+ throw new Error(
+ `wait-for-electron-build: failed to stat ${file}: ${
+ error instanceof Error ? error.message : String(error)
+ }`,
+ { cause: error },
+ );
+ }
+};
+
+/**
+ * Build a "has this dev cycle produced a fresh build?" checker.
+ *
+ * The checker snapshots each required file's mtime at creation time, then
+ * on every call it returns true only when every file exists AND either was
+ * absent in the snapshot or now has a strictly newer mtime. This avoids
+ * treating stale dist artifacts from a previous `pnpm dev` run as "fresh".
+ */
+export const createFreshBuildChecker = (files, readMtime = readMtimeMs) => {
+ const initialMtimes = new Map(files.map((file) => [file, readMtime(file)]));
+
+ return () =>
+ files.every((file) => {
+ const current = readMtime(file);
+ if (current === null) {
+ return false;
+ }
+
+ const initial = initialMtimes.get(file);
+ return initial === null || current > initial;
+ });
+};
+
+export const checkRendererReady = (url) =>
+ new Promise((resolve) => {
+ const request = http.get(url, (response) => {
+ response.resume();
+ resolve(response.statusCode === 200);
+ });
+
+ request.on('error', () => resolve(false));
+ request.setTimeout(1000, () => {
+ request.destroy();
+ resolve(false);
+ });
+ });
+
+/**
+ * Poll until the required build outputs are fresh AND the renderer dev
+ * server is serving a 200, or until `maxWaitMs` elapses. Dependencies are
+ * injectable so the loop can be unit-tested with a virtual clock.
+ */
+export const waitForBuild = async ({
+ requiredFiles = defaultRequiredFiles,
+ rendererUrl = defaultRendererUrl,
+ maxWaitMs = defaultMaxWaitMs,
+ pollIntervalMs = defaultPollIntervalMs,
+ readMtime = readMtimeMs,
+ isRendererReady = () => checkRendererReady(rendererUrl),
+ now = () => Date.now(),
+ delay = sleep,
+} = {}) => {
+ const hasFreshBuild = createFreshBuildChecker(requiredFiles, readMtime);
+ const startedAt = now();
+
+ while (now() - startedAt < maxWaitMs) {
+ if (hasFreshBuild() && (await isRendererReady())) {
+ return true;
+ }
+
+ await delay(pollIntervalMs);
+ }
+
+ return false;
+};
+
+const isDirectInvocation =
+ process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
+
+if (isDirectInvocation) {
+ console.log('Waiting for Midscene Studio shell build output...');
+
+ const ready = await waitForBuild();
+
+ if (ready) {
+ console.log('Midscene Studio shell build is ready.');
+ process.exit(0);
+ }
+
+ console.error(
+ 'Timed out waiting for the Midscene Studio shell build to finish.',
+ );
+ process.exit(1);
+}
diff --git a/apps/studio/src/env.d.ts b/apps/studio/src/env.d.ts
new file mode 100644
index 0000000000..260bcb2a80
--- /dev/null
+++ b/apps/studio/src/env.d.ts
@@ -0,0 +1,9 @@
+import type { ElectronShellApi } from './shared/electron-contract';
+
+declare global {
+ interface Window {
+ electronShell?: ElectronShellApi;
+ }
+}
+
+declare const __APP_VERSION__: string;
diff --git a/apps/studio/src/main/index.ts b/apps/studio/src/main/index.ts
new file mode 100644
index 0000000000..8218889bcf
--- /dev/null
+++ b/apps/studio/src/main/index.ts
@@ -0,0 +1,150 @@
+import { existsSync } from 'node:fs';
+import path from 'node:path';
+import { IPC_CHANNELS } from '@shared/electron-contract';
+import {
+ BrowserWindow,
+ type NativeImage,
+ app,
+ ipcMain,
+ nativeImage,
+} from 'electron';
+import type { TitleBarOverlay } from 'electron';
+
+/**
+ * Main process owns native shell concerns only.
+ * Future device discovery / agent hosting should be bootstrapped from here and
+ * delegated to a dedicated Node-side service, not imported into the renderer.
+ */
+
+let mainWindow: BrowserWindow | null = null;
+let cachedAppIcon: NativeImage | null = null;
+
+const getRendererEntryPath = () =>
+ path.join(__dirname, '../renderer/index.html');
+
+const getPreloadEntryPath = () =>
+ path.join(__dirname, '../preload/preload.cjs');
+
+const getAppIconPath = () => {
+ const candidatePaths = [
+ path.resolve(process.resourcesPath, 'assets/midscene-icon.png'),
+ path.resolve(app.getAppPath(), 'assets/midscene-icon.png'),
+ path.resolve(__dirname, '../assets/midscene-icon.png'),
+ ];
+
+ const iconPath = candidatePaths.find((candidatePath) =>
+ existsSync(candidatePath),
+ );
+
+ if (!iconPath) {
+ throw new Error(
+ `Midscene Studio app icon not found. Checked: ${candidatePaths.join(', ')}`,
+ );
+ }
+
+ return iconPath;
+};
+
+const getAppIcon = () => {
+ if (cachedAppIcon) {
+ return cachedAppIcon;
+ }
+
+ const icon = nativeImage.createFromPath(getAppIconPath());
+
+ if (icon.isEmpty()) {
+ throw new Error('Midscene Studio app icon could not be loaded.');
+ }
+
+ cachedAppIcon = icon;
+ return icon;
+};
+
+const getBackgroundColor = () =>
+ process.platform === 'darwin' ? '#00000000' : '#eef1f5';
+
+const getTitleBarOverlay = (): TitleBarOverlay => ({
+ color: '#00000000',
+ height: 56,
+ symbolColor: '#17212b',
+});
+
+const createMainWindow = () => {
+ const rendererDevUrl = process.env.MIDSCENE_STUDIO_RENDERER_URL;
+ const appIcon = getAppIcon();
+ const window = new BrowserWindow({
+ width: 1440,
+ height: 900,
+ minWidth: 1180,
+ minHeight: 760,
+ backgroundColor: getBackgroundColor(),
+ autoHideMenuBar: true,
+ show: false,
+ titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'hidden',
+ titleBarOverlay:
+ process.platform === 'darwin' ? undefined : getTitleBarOverlay(),
+ trafficLightPosition:
+ process.platform === 'darwin' ? { x: 18, y: 18 } : undefined,
+ transparent: process.platform === 'darwin',
+ vibrancy: process.platform === 'darwin' ? 'under-window' : undefined,
+ visualEffectState: process.platform === 'darwin' ? 'active' : undefined,
+ backgroundMaterial: process.platform === 'win32' ? 'mica' : undefined,
+ icon: appIcon,
+ webPreferences: {
+ contextIsolation: true,
+ nodeIntegration: false,
+ preload: getPreloadEntryPath(),
+ sandbox: false,
+ },
+ });
+
+ window.once('ready-to-show', () => {
+ window.show();
+ });
+
+ if (rendererDevUrl) {
+ window.loadURL(rendererDevUrl);
+ } else {
+ window.loadFile(getRendererEntryPath());
+ }
+
+ mainWindow = window;
+};
+
+const registerIpcHandlers = () => {
+ ipcMain.handle(IPC_CHANNELS.minimizeWindow, () => {
+ mainWindow?.minimize();
+ });
+ ipcMain.handle(IPC_CHANNELS.toggleMaximizeWindow, () => {
+ if (!mainWindow) return;
+ if (mainWindow.isMaximized()) {
+ mainWindow.unmaximize();
+ } else {
+ mainWindow.maximize();
+ }
+ });
+ ipcMain.handle(IPC_CHANNELS.closeWindow, () => {
+ mainWindow?.close();
+ });
+};
+
+app.whenReady().then(() => {
+ if (process.platform === 'darwin' && app.dock) {
+ app.dock.setIcon(getAppIcon());
+ }
+
+ registerIpcHandlers();
+ createMainWindow();
+
+ app.on('activate', () => {
+ if (BrowserWindow.getAllWindows().length === 0) {
+ createMainWindow();
+ }
+ });
+});
+
+app.on('window-all-closed', () => {
+ if (process.platform !== 'darwin') {
+ app.quit();
+ }
+});
diff --git a/apps/studio/src/preload/index.ts b/apps/studio/src/preload/index.ts
new file mode 100644
index 0000000000..a29e591512
--- /dev/null
+++ b/apps/studio/src/preload/index.ts
@@ -0,0 +1,16 @@
+import { type ElectronShellApi, IPC_CHANNELS } from '@shared/electron-contract';
+import { contextBridge, ipcRenderer } from 'electron';
+
+/**
+ * Preload is intentionally thin.
+ * It exposes a typed bridge and keeps Electron access out of the renderer.
+ */
+
+const electronShellApi: ElectronShellApi = {
+ closeWindow: () => ipcRenderer.invoke(IPC_CHANNELS.closeWindow),
+ minimizeWindow: () => ipcRenderer.invoke(IPC_CHANNELS.minimizeWindow),
+ toggleMaximizeWindow: () =>
+ ipcRenderer.invoke(IPC_CHANNELS.toggleMaximizeWindow),
+};
+
+contextBridge.exposeInMainWorld('electronShell', electronShellApi);
diff --git a/apps/studio/src/renderer/App.css b/apps/studio/src/renderer/App.css
new file mode 100644
index 0000000000..4818c4c409
--- /dev/null
+++ b/apps/studio/src/renderer/App.css
@@ -0,0 +1,29 @@
+@layer theme, base, components, utilities;
+@import "tailwindcss/theme.css" layer(theme);
+@import "tailwindcss/utilities.css" layer(utilities);
+
+@layer base {
+ html,
+ body,
+ #root {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+ background: transparent;
+ }
+
+ body {
+ overscroll-behavior: none;
+ }
+}
+
+@layer utilities {
+ .app-drag {
+ -webkit-app-region: drag;
+ }
+
+ .app-no-drag {
+ -webkit-app-region: no-drag;
+ }
+}
diff --git a/apps/studio/src/renderer/App.tsx b/apps/studio/src/renderer/App.tsx
new file mode 100644
index 0000000000..c404196194
--- /dev/null
+++ b/apps/studio/src/renderer/App.tsx
@@ -0,0 +1,10 @@
+import './App.css';
+import { ShellLayout } from './components';
+
+export default function App() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/studio/src/renderer/assets/index.ts b/apps/studio/src/renderer/assets/index.ts
new file mode 100644
index 0000000000..04be1d1a18
--- /dev/null
+++ b/apps/studio/src/renderer/assets/index.ts
@@ -0,0 +1,26 @@
+export const assetUrls = {
+ main: {
+ chat: new URL('./main-chat.png', import.meta.url).href,
+ device: new URL('./main-device.png', import.meta.url).href,
+ disconnect: new URL('./main-disconnect.png', import.meta.url).href,
+ phoneScreen: new URL('./main-phone-screen.png', import.meta.url).href,
+ },
+ playground: {
+ action: new URL('./playground-action.png', import.meta.url).href,
+ actionChevron: new URL('./playground-action-chevron.png', import.meta.url)
+ .href,
+ history: new URL('./playground-history.png', import.meta.url).href,
+ midsceneIcon: new URL('../../../assets/midscene-icon.png', import.meta.url)
+ .href,
+ send: new URL('./playground-send.png', import.meta.url).href,
+ tool: new URL('./playground-tool.png', import.meta.url).href,
+ },
+ sidebar: {
+ computer: new URL('./sidebar-computer.png', import.meta.url).href,
+ harmony: new URL('./sidebar-harmony.png', import.meta.url).href,
+ ios: new URL('./sidebar-ios.png', import.meta.url).href,
+ overview: new URL('./sidebar-overview.png', import.meta.url).href,
+ settings: new URL('./sidebar-settings.png', import.meta.url).href,
+ web: new URL('./sidebar-web.png', import.meta.url).href,
+ },
+} as const;
diff --git a/apps/studio/src/renderer/assets/main-chat.png b/apps/studio/src/renderer/assets/main-chat.png
new file mode 100644
index 0000000000..959441d96a
Binary files /dev/null and b/apps/studio/src/renderer/assets/main-chat.png differ
diff --git a/apps/studio/src/renderer/assets/main-device.png b/apps/studio/src/renderer/assets/main-device.png
new file mode 100644
index 0000000000..24ef5eef65
Binary files /dev/null and b/apps/studio/src/renderer/assets/main-device.png differ
diff --git a/apps/studio/src/renderer/assets/main-disconnect.png b/apps/studio/src/renderer/assets/main-disconnect.png
new file mode 100644
index 0000000000..c0659108c3
Binary files /dev/null and b/apps/studio/src/renderer/assets/main-disconnect.png differ
diff --git a/apps/studio/src/renderer/assets/main-phone-screen.png b/apps/studio/src/renderer/assets/main-phone-screen.png
new file mode 100644
index 0000000000..0cf97ac7d6
Binary files /dev/null and b/apps/studio/src/renderer/assets/main-phone-screen.png differ
diff --git a/apps/studio/src/renderer/assets/playground-action-chevron.png b/apps/studio/src/renderer/assets/playground-action-chevron.png
new file mode 100644
index 0000000000..7b37df9b21
Binary files /dev/null and b/apps/studio/src/renderer/assets/playground-action-chevron.png differ
diff --git a/apps/studio/src/renderer/assets/playground-action.png b/apps/studio/src/renderer/assets/playground-action.png
new file mode 100644
index 0000000000..8b862ef413
Binary files /dev/null and b/apps/studio/src/renderer/assets/playground-action.png differ
diff --git a/apps/studio/src/renderer/assets/playground-history.png b/apps/studio/src/renderer/assets/playground-history.png
new file mode 100644
index 0000000000..630a358fa5
Binary files /dev/null and b/apps/studio/src/renderer/assets/playground-history.png differ
diff --git a/apps/studio/src/renderer/assets/playground-send.png b/apps/studio/src/renderer/assets/playground-send.png
new file mode 100644
index 0000000000..abea6d051e
Binary files /dev/null and b/apps/studio/src/renderer/assets/playground-send.png differ
diff --git a/apps/studio/src/renderer/assets/playground-tool.png b/apps/studio/src/renderer/assets/playground-tool.png
new file mode 100644
index 0000000000..bca142ebda
Binary files /dev/null and b/apps/studio/src/renderer/assets/playground-tool.png differ
diff --git a/apps/studio/src/renderer/assets/sidebar-computer.png b/apps/studio/src/renderer/assets/sidebar-computer.png
new file mode 100644
index 0000000000..39af4a8273
Binary files /dev/null and b/apps/studio/src/renderer/assets/sidebar-computer.png differ
diff --git a/apps/studio/src/renderer/assets/sidebar-harmony.png b/apps/studio/src/renderer/assets/sidebar-harmony.png
new file mode 100644
index 0000000000..0776a78747
Binary files /dev/null and b/apps/studio/src/renderer/assets/sidebar-harmony.png differ
diff --git a/apps/studio/src/renderer/assets/sidebar-ios.png b/apps/studio/src/renderer/assets/sidebar-ios.png
new file mode 100644
index 0000000000..8d31fe3ce8
Binary files /dev/null and b/apps/studio/src/renderer/assets/sidebar-ios.png differ
diff --git a/apps/studio/src/renderer/assets/sidebar-overview.png b/apps/studio/src/renderer/assets/sidebar-overview.png
new file mode 100644
index 0000000000..97bcf63c0a
Binary files /dev/null and b/apps/studio/src/renderer/assets/sidebar-overview.png differ
diff --git a/apps/studio/src/renderer/assets/sidebar-settings.png b/apps/studio/src/renderer/assets/sidebar-settings.png
new file mode 100644
index 0000000000..fb5158727e
Binary files /dev/null and b/apps/studio/src/renderer/assets/sidebar-settings.png differ
diff --git a/apps/studio/src/renderer/assets/sidebar-web.png b/apps/studio/src/renderer/assets/sidebar-web.png
new file mode 100644
index 0000000000..d0e0635427
Binary files /dev/null and b/apps/studio/src/renderer/assets/sidebar-web.png differ
diff --git a/apps/studio/src/renderer/components/MainContent/index.tsx b/apps/studio/src/renderer/components/MainContent/index.tsx
new file mode 100644
index 0000000000..f6c7c72866
--- /dev/null
+++ b/apps/studio/src/renderer/components/MainContent/index.tsx
@@ -0,0 +1,59 @@
+import { assetUrls } from '../../assets';
+
+export default function MainContent() {
+ return (
+
+
+
+
+
+
+
+ 三星 Galaxy S26 Ultra
+
+
+
+
+ Live
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/studio/src/renderer/components/Playground/index.tsx b/apps/studio/src/renderer/components/Playground/index.tsx
new file mode 100644
index 0000000000..f33d42ff5d
--- /dev/null
+++ b/apps/studio/src/renderer/components/Playground/index.tsx
@@ -0,0 +1,75 @@
+import { assetUrls } from '../../assets';
+
+export default function Playground() {
+ return (
+
+
+
+ Playground
+
+
+
+
+
+
+ Welcome to Midscene.js Playground!
+
+
+ {`This is a panel for experimenting and testing Midscene.js features.
+You can use natural language instructions to operate the web page, such as clicking buttons, filling in forms, querying information, etc.
+Please enter your instructions in the input box below to start experiencing.`}
+
+
+
+
+
+
+ Type a message
+
+
+
+
+
+
+ Action
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/studio/src/renderer/components/ShellLayout/index.tsx b/apps/studio/src/renderer/components/ShellLayout/index.tsx
new file mode 100644
index 0000000000..9516fbdd6d
--- /dev/null
+++ b/apps/studio/src/renderer/components/ShellLayout/index.tsx
@@ -0,0 +1,24 @@
+import MainContent from '../MainContent';
+import Playground from '../Playground';
+import Sidebar, { SidebarFooter } from '../Sidebar';
+
+export default function ShellLayout() {
+ return (
+