Skill: Daytona Electron Test Drive the real OpenWork Electron app inside a Daytona sandbox via CDP browser tools. Covers workspace creation, session interaction, settings verification, and bug reproduction. When to use - User says "test on Daytona", "run the app on Daytona", "Daytona dry run" - User wants to reproduce a bug in the real Electron app remotely - User wants to verify a UI flow end-to-end without local Electron Fastest path: the script Run the helper script from the repo root. It creates a Daytona VNC-capable sandbox from the reusable snapshot when present, checks out the ref, con…

); });\n return key ? el[key] : null;\n }\n var all = document.querySelectorAll('span,div,p');\n var p = null;\n for (var i = 0; i \u003c all.length; i++) {\n if (all[i].textContent.indexOf('No folder') !== -1) { p = all[i]; break; }\n }\n if (!p) return {err: 'no placeholder'};\n var fiber = findFiber(p);\n while (fiber) {\n var name = (fiber.elementType && fiber.elementType.name) || (fiber.type && fiber.type.name) || '';\n if (name === 'CreateWorkspaceModal') break;\n fiber = fiber.return;\n }\n if (!fiber) return {err: 'no fiber'};\n var hook = fiber.memoizedState;\n while (hook) {\n if (hook.queue && hook.queue.dispatch) {\n hook.queue.dispatch({ key: 'selectedFolder', value: '/workspace/hello' });\n hook.queue.dispatch({ key: 'pickingFolder', value: false });\n return {ok: true};\n }\n hook = hook.next;\n }\n return {err: 'no dispatch'};\n})())\n```\n\nThe reducer uses `{ key, value }` actions. NOT direct state replacement.\n\n4. **Click \"Create Workspace\":**\n```js\n(function() { var btns = document.querySelectorAll('button'); for (var i = 0; i \u003c btns.length; i++) { if (btns[i].textContent.trim() === 'Create Workspace' && !btns[i].disabled) { btns[i].click(); return 'clicked'; } } return 'not found'; })()\n```\n\n5. **Wait 10-12s.** Verify:\n - URL contains `#/workspace/ws_`\n - Status bar shows \"OpenWork Ready\"\n - opencode process running: `daytona exec \"$SANDBOX\" -- \"bash -lc 'ps aux | grep opencode | grep -v grep'\"`\n\n## UI automation selector map\n\nBefore guessing selectors, check the owning component. Prefer ARIA labels,\nbutton text, and input placeholders over brittle CSS classes. Use React fiber\nonly when bypassing native file pickers.\n\n| Control | Stable selector/search | Source file |\n|---|---|---|\n| Settings button | `button[aria-label=\"Settings\"]` | `apps/app/src/react-app/domains/session/chat/status-bar.tsx` |\n| Back to app | button text `Back to app` | `apps/app/src/react-app/domains/settings/shell/settings-shell.tsx` |\n| New task | `button[aria-label=\"New task\"]` | `apps/app/src/react-app/domains/session/sidebar/app-sidebar.tsx` |\n| Run task | button text `Run task` | `apps/app/src/react-app/domains/session/surface/composer/composer.tsx` |\n| Model selector | `button[aria-label=\"Change model\"]` | `apps/app/src/react-app/domains/session/surface/composer/composer.tsx` |\n| Composer editor | `[contenteditable=\"true\"][data-lexical-editor=\"true\"]` | `apps/app/src/react-app/domains/session/surface/composer/editor.tsx` |\n| AI Providers tab | button text `AI Providers` | `apps/app/src/react-app/domains/settings/shell/settings-page.tsx` |\n| Connect provider | button text `Connect provider` | `apps/app/src/react-app/domains/settings/pages/ai-view.tsx` |\n| Provider search | `input[placeholder=\"Filter providers by name or ID\"]` | `apps/app/src/react-app/domains/connections/provider-auth/provider-auth-modal.tsx` |\n| Manual key option | button containing `Manually enter API Key` | `provider-auth-modal.tsx` |\n| API key input | `input[type=\"password\"][placeholder=\"sk-...\"]` | `provider-auth-modal.tsx` |\n| Save key | button text `Save key` | `provider-auth-modal.tsx` |\n\nReusable click helpers:\n\n```js\n// Click exact button text.\n(function(text) { var b = Array.from(document.querySelectorAll('button')).find(function(el) { return el.textContent.trim() === text && !el.disabled; }); if (!b) return 'not found: ' + text; b.click(); return 'clicked: ' + text; })('AI Providers')\n```\n\n```js\n// Click an ARIA-labeled button/link.\n(function(label) { var el = Array.from(document.querySelectorAll('button,a')).find(function(node) { return node.getAttribute('aria-label') === label && !node.disabled; }); if (!el) return 'not found: ' + label; el.click(); return 'clicked: ' + label; })('Settings')\n```\n\n```js\n// Set a React-controlled input.\n(function(selector, value) { var input = document.querySelector(selector); if (!input) return 'not found: ' + selector; Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(input, value); input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value })); return 'set: ' + selector; })('input[placeholder=\"Filter providers by name or ID\"]', 'openai')\n```\n\n```js\n// Paste text into the Lexical composer. Prefer this over execCommand in Electron/CDP.\n(function(text) { var editor = document.querySelector('[contenteditable=\"true\"][data-lexical-editor=\"true\"]'); if (!editor) return 'no editor'; editor.focus(); var data = new DataTransfer(); data.setData('text/plain', text); editor.dispatchEvent(new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: data })); return editor.innerText; })('Reply with exactly: Daytona UI key OK')\n```\n\n## Connect OpenAI through the UI\n\nUse this when the user provides a temporary key and asks to test real model\nsessions. Do not write the key into docs or repo files.\n\n1. Open Settings using `button[aria-label=\"Settings\"]`.\n2. Click `AI Providers`.\n3. Click `Connect provider`.\n4. Set `input[placeholder=\"Filter providers by name or ID\"]` to `openai`.\n5. Click the provider row containing `OpenAI` and `openai`.\n6. Click `Manually enter API Key`.\n7. Set `input[type=\"password\"][placeholder=\"sk-...\"]` to the key.\n8. Click `Save key`.\n9. Verify text includes `2 providers connected`, `OpenAI`, and `Disconnect`.\n10. Click `Pick a new default?`, expand `OpenAI`, select `Default model`, and click `GPT-5.5gpt-5.5`.\n11. Return to app, create a session, paste a prompt into the composer, and click `Run task`.\n\nExpected successful session message metadata: provider `openai`, model `gpt-5.5`, variant `medium`.\n\n## Session interaction\n\n### Prerequisites: API key for real LLM sessions\n\nTo test real sessions (not just UI flow), the opencode sidecar needs an LLM\nprovider key. The easiest is OpenAI:\n\n```bash\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'cd /workspace/hello && node -e \\\"\n const fs = require(\\\\\\\"fs\\\\\\\");\n const p = \\\\\\\"opencode.jsonc\\\\\\\";\n let c = JSON.parse(fs.readFileSync(p, \\\\\\\"utf8\\\\\\\").replace(/^\\\\\\\\/\\\\\\\\/.*$/gm, \\\\\\\"\\\\\\\"));\n c.provider = c.provider || {};\n c.provider.openai = { options: { apiKey: process.env.KEY } };\n fs.writeFileSync(p, JSON.stringify(c, null, 2));\n\\\" '\"\n```\n\nSet `KEY=sk-proj-...` in the command above. After writing the config, you\n**must restart all services** (see \"Injecting API keys\" section below) for\nopencode to pick up the new provider.\n\nTo switch models in the UI, click the model name in the bottom bar (e.g.\n\"Big Pickle\") and select the desired model (e.g. GPT-5.5).\n\n### Type in the Lexical composer\n\n```js\n(function() {\n var editor = document.querySelector('[contenteditable=true]');\n if (!editor) return 'no editor';\n editor.focus();\n document.execCommand('selectAll', false, null);\n document.execCommand('insertText', false, 'YOUR PROMPT HERE');\n return 'typed';\n})()\n```\n\n**MUST use `document.execCommand('insertText', ...)`.**\nDirect `textContent =` or `innerHTML =` does NOT trigger Lexical state updates.\n\n### Click Run task\n\n```js\n(function() { var btns = document.querySelectorAll('button'); for (var i = 0; i \u003c btns.length; i++) { if (btns[i].textContent.indexOf('Run task') !== -1 && !btns[i].disabled) { btns[i].click(); return 'clicked'; } } return 'not found'; })()\n```\n\n### Check response\n\n```js\ndocument.body.innerText.substring(0, 3000)\n```\n\n## Settings navigation\n\n**Open settings** (gear icon):\n```js\n(function() { var el = Array.from(document.querySelectorAll('button,a')).find(function(node) { return node.getAttribute('aria-label') === 'Settings'; }); if (!el) return 'not found'; el.click(); return 'clicked'; })()\n```\n\n**Navigate to a panel** (e.g. AI Providers):\n```js\n(function() { var btn = Array.from(document.querySelectorAll('button')).find(function(el) { return el.textContent.trim() === 'AI Providers'; }); if (!btn) return 'not found'; btn.click(); return 'clicked'; })()\n```\n\n**Back to app:**\n```js\n(function() { var btn = Array.from(document.querySelectorAll('button')).find(function(el) { return el.textContent.trim() === 'Back to app'; }); if (!btn) return 'not found'; btn.click(); return 'clicked'; })()\n```\n\n## Window management (minimize/restore testing)\n\nInstall xdotool first:\n```bash\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'apt-get update && apt-get install -y xdotool'\"\n```\n\nThen:\n```bash\n# Minimize\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'DISPLAY=:99 xdotool search --name OpenWork windowminimize'\"\n\n# Restore\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'DISPLAY=:99 xdotool search --name OpenWork windowactivate'\"\n```\n\n## API keys for provider evals\n\nDo not edit workspace config or print keys. Create/populate the reusable\nDaytona volume once from the repo root:\n\n```bash\nbash .devcontainer/setup-daytona-secrets-volume.sh .newtoken\n```\n\nEvery Daytona eval sandbox mounts `openwork-eval-secrets:/daytona-secrets` and\n`/opt/openwork-daytona/start-daytona-electron.sh` sources\n`/daytona-secrets/openai.env` before Electron starts. If you update the volume while a sandbox is already\nrunning, restart Electron so the env is reloaded:\n\n```bash\n# Step 1: kill Electron/runtime children\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'pkill -f electron || true; pkill -f electron-dev || true; pkill -f opencode || true'\"\n\n# Step 2: wait, then restart Electron (separate exec call)\nsleep 3\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'cd /workspace && bash /opt/openwork-daytona/start-daytona-electron.sh --detach'\"\n```\n\n**GOTCHA:** Do NOT chain `pkill` and the restart in the same\n`daytona exec` call. `pkill -f electron` sends SIGTERM to the exec session\nitself (because the command string matches). The restart never runs.\nAlways use two separate `daytona exec` calls with a `sleep` between them.\n\n## Ports reference\n\n| Service | Port | Description |\n|-----------|------|------------------------------------------|\n| noVNC | 6080 | See the Electron app visually |\n| Vite HMR | 5173 | React UI hot reload |\n| CDP | 9825 | Chrome DevTools Protocol for automation |\n| Den Web | 3005 | Admin dashboard (needs MySQL) |\n| Den API | 8788 | Control plane (needs MySQL) |\n\n## Two-sandbox Den + Electron marketplace evals\n\nUse this when testing Cloud Marketplace, desktop policies, or org-managed\nextension flows end-to-end.\n\n1. Start the Den server sandbox:\n```bash\nbash .devcontainer/test-server-on-daytona.sh \u003cbranch-or-commit>\n```\n\n2. Seed the server sandbox with demo org, marketplace, and plugin data. The seed\nmust use the same encryption key as `.devcontainer/start-daytona-server.sh`, and\n`@openwork/email` must be built before the seed imports Den email helpers:\n```bash\ndaytona exec \u003cserver-sandbox> -- 'cd /workspace && pnpm --filter @openwork/email build && cd /workspace/ee/apps/den-api && OPENWORK_DEV_MODE=1 DATABASE_URL=mysql://root:[email protected]:3306/openwork_den DEN_DB_ENCRYPTION_KEY=daytona-den-db-encryption-key-please-change-1234567890 BETTER_AUTH_SECRET=local-dev-secret-not-for-production-use!! BETTER_AUTH_URL=http://localhost:3005 pnpm exec tsx scripts/seed-demo-org.ts --reset'\n```\n\n3. Start Electron against the printed Den Web/API URLs:\n```bash\nbash .devcontainer/test-on-daytona.sh \u003cbranch-or-commit> --den-base-url \u003cDEN_WEB_URL> --den-api-base-url \u003cDEN_API_URL> --record-video --recording-name \u003cname>\n```\n\n4. Sign in from Electron using the seeded demo account. Create a desktop handoff\ngrant from the Den API, paste the `openwork://den-auth?...` URL into Cloud\nAccount -> `Paste sign-in code`, and choose `Acme Robotics`:\n```bash\nTOKEN=$(curl -s -X POST '\u003cDEN_API_URL>/api/auth/sign-in/email' -H 'content-type: application/json' --data '{\"email\":\"[email protected]\",\"password\":\"OpenWorkDemo123!\"}' | node -e 'let s=\"\";process.stdin.on(\"data\",c=>s+=c);process.stdin.on(\"end\",()=>process.stdout.write(JSON.parse(s).token))')\ncurl -s -X POST '\u003cDEN_API_URL>/v1/auth/desktop-handoff' -H \"authorization: Bearer $TOKEN\" -H 'content-type: application/json' --data '{\"desktopScheme\":\"openwork\"}'\n```\n\n5. Open Settings -> Extensions -> Marketplace and run the marketplace install,\nremove, search, and filter flows against the seeded marketplace packages.\n\n## Troubleshooting\n\n**OOM during pnpm install or Vite esbuild crash (EPIPE):**\nYou used `--memory 1` (default). Always `--memory 8`.\n\n**Electron exits with \"Running as root without --no-sandbox\":**\nThe devcontainer sets `ELECTRON_DISABLE_SANDBOX=1`. If running Electron\nmanually, pass `--no-sandbox` or set the env var.\n\n**Generic DBus errors in Electron logs:**\nDBus warnings are expected in Daytona/Linux containers. They are not fatal if\nyou also see `DevTools listening on ws://127.0.0.1:9825/...` and an OpenWork\nwindow in noVNC.\n\n**GPU process errors in Electron logs:**\n`Exiting GPU process due to errors during initialization` is common under Xvfb.\nIt is not fatal if Chromium falls back and the window appears. If CDP never\nprints `DevTools listening`, check `/tmp/electron.log` and restart Electron.\n\n**\"bun: not found\" during dev:electron:**\nThe sidecar prep script uses bun. The devcontainer Dockerfile installs it\nglobally. If you built a custom Dockerfile, add `RUN npm install -g bun`.\n\n**\"xauth command not found\":**\n`apt-get install -y xauth` (already in the devcontainer Dockerfile).\n\n**CDP shows no targets after 60s:**\nCheck `/tmp/electron.log` and `/tmp/vite.log`:\n```bash\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'tail -80 /tmp/electron.log'\"\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'tail -80 /tmp/vite.log'\"\n```\n\nThe app log line `[openwork] Electron CDP exposed at http://127.0.0.1:9825`\nmeans OpenWork requested CDP. The real success marker is Chromium's own line:\n`DevTools listening on ws://127.0.0.1:9825/devtools/browser/...`.\n\n**opencode sidecar not restarting after kill:**\nThe Electron runtime manager does NOT auto-detect sidecar death. You must\nrestart the entire Electron process.\n\n**`daytona exec` with `pkill` kills the exec session:**\nThe process pattern match hits the exec wrapper. Always split kill and\nrestart into separate `daytona exec` calls.\n\n**Blank Electron window (empty `\u003cdiv id=\"root\">\u003c/div>`):**\nVite crashed (check `/tmp/vite.log`). Usually memory pressure. Verify\n`free -m` shows >2 GB available.\n\n**noVNC URL says sandbox not found:**\nPreview URLs are not stable. Regenerate the URL:\n```bash\ndaytona preview-url \"$SANDBOX\" -p 6080\n```\n\n**Electron starts twice or CDP says address already in use:**\nKill the old Electron process before restarting:\n```bash\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'pkill -f electron || true; pkill -f electron-dev || true'\"\n```\n\n## Teardown\n\n```bash\ndaytona delete \"$SANDBOX\"\n```\n---","attachment_filenames":[],"attachments":[],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Skill: Daytona Electron Test","type":"text"}]},{"type":"paragraph","content":[{"text":"Drive the real OpenWork Electron app inside a Daytona sandbox via CDP browser tools. Covers workspace creation, session interaction, settings verification, and bug reproduction.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to use","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User says \"test on Daytona\", \"run the app on Daytona\", \"Daytona dry run\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User wants to reproduce a bug in the real Electron app remotely","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User wants to verify a UI flow end-to-end without local Electron","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Fastest path: the script","type":"text"}]},{"type":"paragraph","content":[{"text":"Run the helper script from the repo root. It creates a Daytona VNC-capable sandbox from the reusable ","type":"text"},{"text":"openwork-eval-vnc","type":"text","marks":[{"type":"code_inline"}]},{"text":" snapshot when present, checks out the ref, conditionally installs deps, starts XFCE/noVNC, Vite, Electron, and waits for CDP:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash .devcontainer/test-on-daytona.sh [branch-or-commit]","type":"text"}]},{"type":"paragraph","content":[{"text":"It prints the CDP and noVNC URLs at the end. Then use ","type":"text"},{"text":"browser_list","type":"text","marks":[{"type":"code_inline"}]},{"text":" to connect. Refresh the snapshot with ","type":"text"},{"text":"bash .devcontainer/create-daytona-openwork-snapshot.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" when dependencies or base setup change. The snapshot excludes ","type":"text"},{"text":"node_modules","type":"text","marks":[{"type":"code_inline"}]},{"text":"; dependency installs reuse the ","type":"text"},{"text":"openwork-eval-pnpm-store","type":"text","marks":[{"type":"code_inline"}]},{"text":" volume. For OpenAI flows, create the reusable secrets volume once with ","type":"text"},{"text":"bash .devcontainer/setup-daytona-secrets-volume.sh .newtoken","type":"text","marks":[{"type":"code_inline"}]},{"text":"; future Daytona sandboxes mount ","type":"text"},{"text":"openwork-eval-secrets:/daytona-secrets","type":"text","marks":[{"type":"code_inline"}]},{"text":" automatically.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Manual debugging","type":"text"}]},{"type":"paragraph","content":[{"text":"Do not copy raw Daytona create/start commands into new docs or skills. Keep the single maintained provisioning path in ","type":"text"},{"text":".devcontainer/test-on-daytona.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" and debug by inspecting its logs:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"daytona exec \u003csandbox> -- 'tail -80 /tmp/start-vnc.log'\ndaytona exec \u003csandbox> -- 'tail -80 /tmp/vite.log'\ndaytona exec \u003csandbox> -- 'tail -80 /tmp/electron.log'","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Get URLs","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Electron CDP (automation) -- THIS IS WHAT browser_list CONNECTS TO\ndaytona preview-url \"$SANDBOX\" -p 9825\n\n# noVNC (visual access in your browser)\ndaytona preview-url \"$SANDBOX\" -p 6080","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Connect browser tools","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"browser_list({ browser_url: \"\u003cCDP_URL>\" })","type":"text"}]},{"type":"paragraph","content":[{"text":"Should show: ","type":"text"},{"text":"[target_id] OpenWork http://localhost:5173/#/welcome","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"6. Verify it's real Electron (not plain Chromium)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"browser_eval({ expression: \"navigator.userAgent\" })","type":"text"}]},{"type":"paragraph","content":[{"text":"Must contain ","type":"text"},{"text":"Electron/","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Creating a workspace through the UI","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Prepare the directory first","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"daytona exec \"$SANDBOX\" -- \"bash -lc 'mkdir -p /workspace/hello'\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Drive the modal","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Click \"Get started\":","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"(function() { var btns = document.querySelectorAll('button'); for (var i = 0; i \u003c btns.length; i++) { if (btns[i].textContent.indexOf('Get started') !== -1) { btns[i].click(); return 'clicked'; } } return 'not found'; })()","type":"text"}]},{"type":"ordered_list","attrs":{"order":2,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Click \"Local workspace\":","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"(function() { var btns = document.querySelectorAll('button'); for (var i = 0; i \u003c btns.length; i++) { if (btns[i].textContent.indexOf('Local workspace') !== -1) { btns[i].click(); return 'clicked'; } } return 'not found'; })()","type":"text"}]},{"type":"ordered_list","attrs":{"order":3,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Inject folder path","type":"text","marks":[{"type":"strong"}]},{"text":" (bypasses the native file picker that can't work headless):","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"JSON.stringify((function() {\n function findFiber(el) {\n var key = Object.keys(el).find(function(k) { return k.startsWith('__reactFiber

Skill: Daytona Electron Test Drive the real OpenWork Electron app inside a Daytona sandbox via CDP browser tools. Covers workspace creation, session interaction, settings verification, and bug reproduction. When to use - User says "test on Daytona", "run the app on Daytona", "Daytona dry run" - User wants to reproduce a bug in the real Electron app remotely - User wants to verify a UI flow end-to-end without local Electron Fastest path: the script Run the helper script from the repo root. It creates a Daytona VNC-capable sandbox from the reusable snapshot when present, checks out the ref, con…

); });\n return key ? el[key] : null;\n }\n var all = document.querySelectorAll('span,div,p');\n var p = null;\n for (var i = 0; i \u003c all.length; i++) {\n if (all[i].textContent.indexOf('No folder') !== -1) { p = all[i]; break; }\n }\n if (!p) return {err: 'no placeholder'};\n var fiber = findFiber(p);\n while (fiber) {\n var name = (fiber.elementType && fiber.elementType.name) || (fiber.type && fiber.type.name) || '';\n if (name === 'CreateWorkspaceModal') break;\n fiber = fiber.return;\n }\n if (!fiber) return {err: 'no fiber'};\n var hook = fiber.memoizedState;\n while (hook) {\n if (hook.queue && hook.queue.dispatch) {\n hook.queue.dispatch({ key: 'selectedFolder', value: '/workspace/hello' });\n hook.queue.dispatch({ key: 'pickingFolder', value: false });\n return {ok: true};\n }\n hook = hook.next;\n }\n return {err: 'no dispatch'};\n})())","type":"text"}]},{"type":"paragraph","content":[{"text":"The reducer uses ","type":"text"},{"text":"{ key, value }","type":"text","marks":[{"type":"code_inline"}]},{"text":" actions. NOT direct state replacement.","type":"text"}]},{"type":"ordered_list","attrs":{"order":4,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Click \"Create Workspace\":","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"(function() { var btns = document.querySelectorAll('button'); for (var i = 0; i \u003c btns.length; i++) { if (btns[i].textContent.trim() === 'Create Workspace' && !btns[i].disabled) { btns[i].click(); return 'clicked'; } } return 'not found'; })()","type":"text"}]},{"type":"ordered_list","attrs":{"order":5,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Wait 10-12s.","type":"text","marks":[{"type":"strong"}]},{"text":" Verify:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"URL contains ","type":"text"},{"text":"#/workspace/ws_","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Status bar shows \"OpenWork Ready\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"opencode process running: ","type":"text"},{"text":"daytona exec \"$SANDBOX\" -- \"bash -lc 'ps aux | grep opencode | grep -v grep'\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"UI automation selector map","type":"text"}]},{"type":"paragraph","content":[{"text":"Before guessing selectors, check the owning component. Prefer ARIA labels, button text, and input placeholders over brittle CSS classes. Use React fiber only when bypassing native file pickers.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Control","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stable selector/search","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Source file","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Settings button","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"button[aria-label=\"Settings\"]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"apps/app/src/react-app/domains/session/chat/status-bar.tsx","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Back to app","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"button text ","type":"text"},{"text":"Back to app","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"apps/app/src/react-app/domains/settings/shell/settings-shell.tsx","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"New task","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"button[aria-label=\"New task\"]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"apps/app/src/react-app/domains/session/sidebar/app-sidebar.tsx","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run task","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"button text ","type":"text"},{"text":"Run task","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"apps/app/src/react-app/domains/session/surface/composer/composer.tsx","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Model selector","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"button[aria-label=\"Change model\"]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"apps/app/src/react-app/domains/session/surface/composer/composer.tsx","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Composer editor","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"[contenteditable=\"true\"][data-lexical-editor=\"true\"]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"apps/app/src/react-app/domains/session/surface/composer/editor.tsx","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AI Providers tab","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"button text ","type":"text"},{"text":"AI Providers","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"apps/app/src/react-app/domains/settings/shell/settings-page.tsx","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Connect provider","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"button text ","type":"text"},{"text":"Connect provider","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"apps/app/src/react-app/domains/settings/pages/ai-view.tsx","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Provider search","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"input[placeholder=\"Filter providers by name or ID\"]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"apps/app/src/react-app/domains/connections/provider-auth/provider-auth-modal.tsx","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manual key option","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"button containing ","type":"text"},{"text":"Manually enter API Key","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"provider-auth-modal.tsx","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"API key input","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"input[type=\"password\"][placeholder=\"sk-...\"]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"provider-auth-modal.tsx","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Save key","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"button text ","type":"text"},{"text":"Save key","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"provider-auth-modal.tsx","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"Reusable click helpers:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"// Click exact button text.\n(function(text) { var b = Array.from(document.querySelectorAll('button')).find(function(el) { return el.textContent.trim() === text && !el.disabled; }); if (!b) return 'not found: ' + text; b.click(); return 'clicked: ' + text; })('AI Providers')","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"// Click an ARIA-labeled button/link.\n(function(label) { var el = Array.from(document.querySelectorAll('button,a')).find(function(node) { return node.getAttribute('aria-label') === label && !node.disabled; }); if (!el) return 'not found: ' + label; el.click(); return 'clicked: ' + label; })('Settings')","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"// Set a React-controlled input.\n(function(selector, value) { var input = document.querySelector(selector); if (!input) return 'not found: ' + selector; Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(input, value); input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value })); return 'set: ' + selector; })('input[placeholder=\"Filter providers by name or ID\"]', 'openai')","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"// Paste text into the Lexical composer. Prefer this over execCommand in Electron/CDP.\n(function(text) { var editor = document.querySelector('[contenteditable=\"true\"][data-lexical-editor=\"true\"]'); if (!editor) return 'no editor'; editor.focus(); var data = new DataTransfer(); data.setData('text/plain', text); editor.dispatchEvent(new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: data })); return editor.innerText; })('Reply with exactly: Daytona UI key OK')","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Connect OpenAI through the UI","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this when the user provides a temporary key and asks to test real model sessions. Do not write the key into docs or repo files.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Open Settings using ","type":"text"},{"text":"button[aria-label=\"Settings\"]","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Click ","type":"text"},{"text":"AI Providers","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Click ","type":"text"},{"text":"Connect provider","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Set ","type":"text"},{"text":"input[placeholder=\"Filter providers by name or ID\"]","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"openai","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Click the provider row containing ","type":"text"},{"text":"OpenAI","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"openai","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Click ","type":"text"},{"text":"Manually enter API Key","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Set ","type":"text"},{"text":"input[type=\"password\"][placeholder=\"sk-...\"]","type":"text","marks":[{"type":"code_inline"}]},{"text":" to the key.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Click ","type":"text"},{"text":"Save key","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify text includes ","type":"text"},{"text":"2 providers connected","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"OpenAI","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"Disconnect","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Click ","type":"text"},{"text":"Pick a new default?","type":"text","marks":[{"type":"code_inline"}]},{"text":", expand ","type":"text"},{"text":"OpenAI","type":"text","marks":[{"type":"code_inline"}]},{"text":", select ","type":"text"},{"text":"Default model","type":"text","marks":[{"type":"code_inline"}]},{"text":", and click ","type":"text"},{"text":"GPT-5.5gpt-5.5","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Return to app, create a session, paste a prompt into the composer, and click ","type":"text"},{"text":"Run task","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Expected successful session message metadata: provider ","type":"text"},{"text":"openai","type":"text","marks":[{"type":"code_inline"}]},{"text":", model ","type":"text"},{"text":"gpt-5.5","type":"text","marks":[{"type":"code_inline"}]},{"text":", variant ","type":"text"},{"text":"medium","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Session interaction","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Prerequisites: API key for real LLM sessions","type":"text"}]},{"type":"paragraph","content":[{"text":"To test real sessions (not just UI flow), the opencode sidecar needs an LLM provider key. The easiest is OpenAI:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"daytona exec \"$SANDBOX\" -- \"bash -lc 'cd /workspace/hello && node -e \\\"\n const fs = require(\\\\\\\"fs\\\\\\\");\n const p = \\\\\\\"opencode.jsonc\\\\\\\";\n let c = JSON.parse(fs.readFileSync(p, \\\\\\\"utf8\\\\\\\").replace(/^\\\\\\\\/\\\\\\\\/.*$/gm, \\\\\\\"\\\\\\\"));\n c.provider = c.provider || {};\n c.provider.openai = { options: { apiKey: process.env.KEY } };\n fs.writeFileSync(p, JSON.stringify(c, null, 2));\n\\\" '\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Set ","type":"text"},{"text":"KEY=sk-proj-...","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the command above. After writing the config, you ","type":"text"},{"text":"must restart all services","type":"text","marks":[{"type":"strong"}]},{"text":" (see \"Injecting API keys\" section below) for opencode to pick up the new provider.","type":"text"}]},{"type":"paragraph","content":[{"text":"To switch models in the UI, click the model name in the bottom bar (e.g. \"Big Pickle\") and select the desired model (e.g. GPT-5.5).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Type in the Lexical composer","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"(function() {\n var editor = document.querySelector('[contenteditable=true]');\n if (!editor) return 'no editor';\n editor.focus();\n document.execCommand('selectAll', false, null);\n document.execCommand('insertText', false, 'YOUR PROMPT HERE');\n return 'typed';\n})()","type":"text"}]},{"type":"paragraph","content":[{"text":"MUST use ","type":"text","marks":[{"type":"strong"}]},{"text":"document.execCommand('insertText', ...)","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":".","type":"text","marks":[{"type":"strong"}]},{"text":" Direct ","type":"text"},{"text":"textContent =","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"innerHTML =","type":"text","marks":[{"type":"code_inline"}]},{"text":" does NOT trigger Lexical state updates.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Click Run task","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"(function() { var btns = document.querySelectorAll('button'); for (var i = 0; i \u003c btns.length; i++) { if (btns[i].textContent.indexOf('Run task') !== -1 && !btns[i].disabled) { btns[i].click(); return 'clicked'; } } return 'not found'; })()","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Check response","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"document.body.innerText.substring(0, 3000)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Settings navigation","type":"text"}]},{"type":"paragraph","content":[{"text":"Open settings","type":"text","marks":[{"type":"strong"}]},{"text":" (gear icon):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"(function() { var el = Array.from(document.querySelectorAll('button,a')).find(function(node) { return node.getAttribute('aria-label') === 'Settings'; }); if (!el) return 'not found'; el.click(); return 'clicked'; })()","type":"text"}]},{"type":"paragraph","content":[{"text":"Navigate to a panel","type":"text","marks":[{"type":"strong"}]},{"text":" (e.g. AI Providers):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"(function() { var btn = Array.from(document.querySelectorAll('button')).find(function(el) { return el.textContent.trim() === 'AI Providers'; }); if (!btn) return 'not found'; btn.click(); return 'clicked'; })()","type":"text"}]},{"type":"paragraph","content":[{"text":"Back to app:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"(function() { var btn = Array.from(document.querySelectorAll('button')).find(function(el) { return el.textContent.trim() === 'Back to app'; }); if (!btn) return 'not found'; btn.click(); return 'clicked'; })()","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Window management (minimize/restore testing)","type":"text"}]},{"type":"paragraph","content":[{"text":"Install xdotool first:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"daytona exec \"$SANDBOX\" -- \"bash -lc 'apt-get update && apt-get install -y xdotool'\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Then:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Minimize\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'DISPLAY=:99 xdotool search --name OpenWork windowminimize'\"\n\n# Restore\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'DISPLAY=:99 xdotool search --name OpenWork windowactivate'\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"API keys for provider evals","type":"text"}]},{"type":"paragraph","content":[{"text":"Do not edit workspace config or print keys. Create/populate the reusable Daytona volume once from the repo root:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash .devcontainer/setup-daytona-secrets-volume.sh .newtoken","type":"text"}]},{"type":"paragraph","content":[{"text":"Every Daytona eval sandbox mounts ","type":"text"},{"text":"openwork-eval-secrets:/daytona-secrets","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"/opt/openwork-daytona/start-daytona-electron.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" sources ","type":"text"},{"text":"/daytona-secrets/openai.env","type":"text","marks":[{"type":"code_inline"}]},{"text":" before Electron starts. If you update the volume while a sandbox is already running, restart Electron so the env is reloaded:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Step 1: kill Electron/runtime children\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'pkill -f electron || true; pkill -f electron-dev || true; pkill -f opencode || true'\"\n\n# Step 2: wait, then restart Electron (separate exec call)\nsleep 3\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'cd /workspace && bash /opt/openwork-daytona/start-daytona-electron.sh --detach'\"","type":"text"}]},{"type":"paragraph","content":[{"text":"GOTCHA:","type":"text","marks":[{"type":"strong"}]},{"text":" Do NOT chain ","type":"text"},{"text":"pkill","type":"text","marks":[{"type":"code_inline"}]},{"text":" and the restart in the same ","type":"text"},{"text":"daytona exec","type":"text","marks":[{"type":"code_inline"}]},{"text":" call. ","type":"text"},{"text":"pkill -f electron","type":"text","marks":[{"type":"code_inline"}]},{"text":" sends SIGTERM to the exec session itself (because the command string matches). The restart never runs. Always use two separate ","type":"text"},{"text":"daytona exec","type":"text","marks":[{"type":"code_inline"}]},{"text":" calls with a ","type":"text"},{"text":"sleep","type":"text","marks":[{"type":"code_inline"}]},{"text":" between them.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Ports reference","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Service","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Port","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"noVNC","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6080","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"See the Electron app visually","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Vite HMR","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5173","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"React UI hot reload","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CDP","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"9825","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Chrome DevTools Protocol for automation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Den Web","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3005","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Admin dashboard (needs MySQL)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Den API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8788","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Control plane (needs MySQL)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Two-sandbox Den + Electron marketplace evals","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this when testing Cloud Marketplace, desktop policies, or org-managed extension flows end-to-end.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Start the Den server sandbox:","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash .devcontainer/test-server-on-daytona.sh \u003cbranch-or-commit>","type":"text"}]},{"type":"ordered_list","attrs":{"order":2,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Seed the server sandbox with demo org, marketplace, and plugin data. The seed must use the same encryption key as ","type":"text"},{"text":".devcontainer/start-daytona-server.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"@openwork/email","type":"text","marks":[{"type":"code_inline"}]},{"text":" must be built before the seed imports Den email helpers:","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"daytona exec \u003cserver-sandbox> -- 'cd /workspace && pnpm --filter @openwork/email build && cd /workspace/ee/apps/den-api && OPENWORK_DEV_MODE=1 DATABASE_URL=mysql://root:[email protected]:3306/openwork_den DEN_DB_ENCRYPTION_KEY=daytona-den-db-encryption-key-please-change-1234567890 BETTER_AUTH_SECRET=local-dev-secret-not-for-production-use!! BETTER_AUTH_URL=http://localhost:3005 pnpm exec tsx scripts/seed-demo-org.ts --reset'","type":"text"}]},{"type":"ordered_list","attrs":{"order":3,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Start Electron against the printed Den Web/API URLs:","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash .devcontainer/test-on-daytona.sh \u003cbranch-or-commit> --den-base-url \u003cDEN_WEB_URL> --den-api-base-url \u003cDEN_API_URL> --record-video --recording-name \u003cname>","type":"text"}]},{"type":"ordered_list","attrs":{"order":4,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sign in from Electron using the seeded demo account. Create a desktop handoff grant from the Den API, paste the ","type":"text"},{"text":"openwork://den-auth?...","type":"text","marks":[{"type":"code_inline"}]},{"text":" URL into Cloud Account -> ","type":"text"},{"text":"Paste sign-in code","type":"text","marks":[{"type":"code_inline"}]},{"text":", and choose ","type":"text"},{"text":"Acme Robotics","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"TOKEN=$(curl -s -X POST '\u003cDEN_API_URL>/api/auth/sign-in/email' -H 'content-type: application/json' --data '{\"email\":\"[email protected]\",\"password\":\"OpenWorkDemo123!\"}' | node -e 'let s=\"\";process.stdin.on(\"data\",c=>s+=c);process.stdin.on(\"end\",()=>process.stdout.write(JSON.parse(s).token))')\ncurl -s -X POST '\u003cDEN_API_URL>/v1/auth/desktop-handoff' -H \"authorization: Bearer $TOKEN\" -H 'content-type: application/json' --data '{\"desktopScheme\":\"openwork\"}'","type":"text"}]},{"type":"ordered_list","attrs":{"order":5,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Open Settings -> Extensions -> Marketplace and run the marketplace install, remove, search, and filter flows against the seeded marketplace packages.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Troubleshooting","type":"text"}]},{"type":"paragraph","content":[{"text":"OOM during pnpm install or Vite esbuild crash (EPIPE):","type":"text","marks":[{"type":"strong"}]},{"text":" You used ","type":"text"},{"text":"--memory 1","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default). Always ","type":"text"},{"text":"--memory 8","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Electron exits with \"Running as root without --no-sandbox\":","type":"text","marks":[{"type":"strong"}]},{"text":" The devcontainer sets ","type":"text"},{"text":"ELECTRON_DISABLE_SANDBOX=1","type":"text","marks":[{"type":"code_inline"}]},{"text":". If running Electron manually, pass ","type":"text"},{"text":"--no-sandbox","type":"text","marks":[{"type":"code_inline"}]},{"text":" or set the env var.","type":"text"}]},{"type":"paragraph","content":[{"text":"Generic DBus errors in Electron logs:","type":"text","marks":[{"type":"strong"}]},{"text":" DBus warnings are expected in Daytona/Linux containers. They are not fatal if you also see ","type":"text"},{"text":"DevTools listening on ws://127.0.0.1:9825/...","type":"text","marks":[{"type":"code_inline"}]},{"text":" and an OpenWork window in noVNC.","type":"text"}]},{"type":"paragraph","content":[{"text":"GPU process errors in Electron logs:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"Exiting GPU process due to errors during initialization","type":"text","marks":[{"type":"code_inline"}]},{"text":" is common under Xvfb. It is not fatal if Chromium falls back and the window appears. If CDP never prints ","type":"text"},{"text":"DevTools listening","type":"text","marks":[{"type":"code_inline"}]},{"text":", check ","type":"text"},{"text":"/tmp/electron.log","type":"text","marks":[{"type":"code_inline"}]},{"text":" and restart Electron.","type":"text"}]},{"type":"paragraph","content":[{"text":"\"bun: not found\" during dev:electron:","type":"text","marks":[{"type":"strong"}]},{"text":" The sidecar prep script uses bun. The devcontainer Dockerfile installs it globally. If you built a custom Dockerfile, add ","type":"text"},{"text":"RUN npm install -g bun","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"\"xauth command not found\":","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"apt-get install -y xauth","type":"text","marks":[{"type":"code_inline"}]},{"text":" (already in the devcontainer Dockerfile).","type":"text"}]},{"type":"paragraph","content":[{"text":"CDP shows no targets after 60s:","type":"text","marks":[{"type":"strong"}]},{"text":" Check ","type":"text"},{"text":"/tmp/electron.log","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"/tmp/vite.log","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"daytona exec \"$SANDBOX\" -- \"bash -lc 'tail -80 /tmp/electron.log'\"\ndaytona exec \"$SANDBOX\" -- \"bash -lc 'tail -80 /tmp/vite.log'\"","type":"text"}]},{"type":"paragraph","content":[{"text":"The app log line ","type":"text"},{"text":"[openwork] Electron CDP exposed at http://127.0.0.1:9825","type":"text","marks":[{"type":"code_inline"}]},{"text":" means OpenWork requested CDP. The real success marker is Chromium's own line: ","type":"text"},{"text":"DevTools listening on ws://127.0.0.1:9825/devtools/browser/...","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"opencode sidecar not restarting after kill:","type":"text","marks":[{"type":"strong"}]},{"text":" The Electron runtime manager does NOT auto-detect sidecar death. You must restart the entire Electron process.","type":"text"}]},{"type":"paragraph","content":[{"text":"daytona exec","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" with ","type":"text","marks":[{"type":"strong"}]},{"text":"pkill","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" kills the exec session:","type":"text","marks":[{"type":"strong"}]},{"text":" The process pattern match hits the exec wrapper. Always split kill and restart into separate ","type":"text"},{"text":"daytona exec","type":"text","marks":[{"type":"code_inline"}]},{"text":" calls.","type":"text"}]},{"type":"paragraph","content":[{"text":"Blank Electron window (empty ","type":"text","marks":[{"type":"strong"}]},{"text":"\u003cdiv id=\"root\">\u003c/div>","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":"):","type":"text","marks":[{"type":"strong"}]},{"text":" Vite crashed (check ","type":"text"},{"text":"/tmp/vite.log","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Usually memory pressure. Verify ","type":"text"},{"text":"free -m","type":"text","marks":[{"type":"code_inline"}]},{"text":" shows >2 GB available.","type":"text"}]},{"type":"paragraph","content":[{"text":"noVNC URL says sandbox not found:","type":"text","marks":[{"type":"strong"}]},{"text":" Preview URLs are not stable. Regenerate the URL:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"daytona preview-url \"$SANDBOX\" -p 6080","type":"text"}]},{"type":"paragraph","content":[{"text":"Electron starts twice or CDP says address already in use:","type":"text","marks":[{"type":"strong"}]},{"text":" Kill the old Electron process before restarting:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"daytona exec \"$SANDBOX\" -- \"bash -lc 'pkill -f electron || true; pkill -f electron-dev || true'\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Teardown","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"daytona delete \"$SANDBOX\"","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"daytona-electron-test","author":"@skillopedia","source":{"stars":15705,"repo_name":"openwork","origin_url":"https://github.com/different-ai/openwork/blob/HEAD/.opencode/skills/daytona-electron-test/SKILL.md","repo_owner":"different-ai","body_sha256":"d1ff31e761bd95d9d899fc9f543b10c4fd9eb5d88cebca9a7ea4439db7d94544","cluster_key":"211452b4834c5283d0f4f0569ae2573362db31ff7886b77f368feda0155666f9","clean_bundle":{"format":"clean-skill-bundle-v1","source":"different-ai/openwork/.opencode/skills/daytona-electron-test/SKILL.md","bundle_sha256":"20e07d1ddff92f869058c04abf3b001cd4ced2237705705fa6daec133523c88c","attachment_count":0,"text_attachments":0,"binary_attachments":0},"cluster_size":1,"skill_md_path":".opencode/skills/daytona-electron-test/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"testing-qa","category_label":"Testing"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"testing-qa","import_tag":"clean-skills-v1","description":"Test the real Electron app on Daytona: create sandbox, start services, connect via CDP, create workspaces, drive sessions, and verify settings. Use when the user says 'test on Daytona', 'run the app on Daytona', 'Daytona dry run', 'test Electron remotely', or 'reproduce on Daytona'."}},"renderedAt":1782987976668}

Skill: Daytona Electron Test Drive the real OpenWork Electron app inside a Daytona sandbox via CDP browser tools. Covers workspace creation, session interaction, settings verification, and bug reproduction. When to use - User says "test on Daytona", "run the app on Daytona", "Daytona dry run" - User wants to reproduce a bug in the real Electron app remotely - User wants to verify a UI flow end-to-end without local Electron Fastest path: the script Run the helper script from the repo root. It creates a Daytona VNC-capable sandbox from the reusable snapshot when present, checks out the ref, con…