rowboat/apps/x/MINI_APPS_PLAN.md
Gagan 7acbf67cd4 feat(mini-apps): add Mini Apps section with sandboxed app runtime
Phase 1 of Mini Apps: a new sidebar section that renders user apps as
premium tiles and opens each in a sandboxed iframe.

- New first-class 'apps' view wired through App.tsx (mirrors bg-tasks)
- 'Mini Apps' sidebar entry
- MiniAppFrame: sandboxed iframe host + window.rowboat bridge
  (getData/onData, getState/setState, scoped callAction; actions stubbed)
- Self-contained vanilla apps (no CDN) so they render offline in the frame
- Sample apps: Twitter (X-style feed + topic filters), Newsletter Digest,
  Competitor Watch
- Premium card gallery: accent themes, decorative patterns, ambient glow,
  light/dark support
2026-06-30 00:30:29 +05:30

9.1 KiB

Mini Apps — Implementation Plan

Status: Phase 1 (UI + rendering, hand-coded apps) in progress. Branch: feature/mini-apps. Working dir: apps/x.

1. What we're building (agreed design)

A Mini App is a user-created, personal app that lives inside Rowboat under a "Mini Apps" sidebar tab, shown as cards; click a card to open and use it like a standalone app.

The settled mental model:

A Mini App = custom UI code (a single self-contained HTML file: React + Tailwind, no build step, runs in a sandboxed iframe) + an agent "backend" that produces fresh data on a trigger + an optional state store + a scoped bridge that lets the UI call real Composio actions.

Key decisions (the "why" behind the architecture):

  • Opens to a finished, populated screen. The agent runs before open (on a trigger); the UI just renders the latest result. Maps onto the existing background-agents engine.
  • Frontend/backend split. Code = frontend, written once. Agent = backend, produces fresh data.json each run. The UI is not regenerated per run. Cheaper, faster, and the UI can't break on refresh.
  • A data contract ties the three pieces together: at creation the builder produces a matched triple — UI code + data schema + agent instructions.
  • Fully custom UI per app (generated code), not a fixed block kit.
  • Real interactivity via a scoped bridge to Composio (each app declares the integrations it may touch; scope drives both enforcement and the auth prompt).
  • Built by whatever engine is selected in the chat box — Code Mode (Claude Code / Codex) if the user has it, else the Copilot.
  • Living apps: refined by chatting; breakage is first-class (app surfaces errors → "fix with copilot").
  • State is an optional provided primitive (per-app persistent store the agent and UI can read/write). Stateless apps ignore it.
  • Personal/local only for now, but each app is a self-contained folder so it can become portable later.

Framework for the apps

Single self-contained index.html rendered in a sandboxed iframe.

Learning (Phase 1): remote CDNs do NOT work inside the sandboxed, opaque-origin srcdoc iframe (React/Tailwind/Babel from unpkg/cdn.tailwindcss failed → blank screen). Apps must be fully self-contained, no network. Phase 1 therefore ships vanilla HTML + CSS + JS (no build, no transpile). The React+Tailwind LLM-friendly target still stands for generation — but via a locally-bundled runtime (esbuild-at-save, libs injected into the HTML), never remote CDNs. The window.rowboat bridge is unchanged either way.

The UI talks to the host through a window.rowboat bridge (the product surface and the security boundary):

rowboat.getData(): Promise<Data>             // latest agent output
rowboat.onData(cb): void                      // re-render on refresh
rowboat.getState() / setState(patch)          // per-app persistent store
rowboat.callAction(scope, action, args)       // scoped Composio call
rowboat.ready()                               // handshake: "send me data"

2. Phased roadmap

  1. Surface + one real app (UI-first — THIS PHASE). "Mini Apps" sidebar tab, card grid, click to open. One hardcoded app (Twitter client) rendered in a sandboxed iframe from a self-contained HTML file, reading static data through a minimal bridge. Proves the experience and the rendering path. No agent, no storage, no real Composio yet.
  2. The scoped bridge. Real interactive buttons → real Composio actions, enforced against a per-app manifest scope; auth prompt on demand.
  3. Generation. Copilot/Code-Mode generates the UI+schema+agent triple from a chat description. Rides the existing chat mode selector + code-with-agents.
  4. Living apps + breakage recovery. Conversational edits; error surfacing.
  5. Scale & polish. Context-overload windowing (e.g. 100 tweets); app notifications (reuse notify-user).
  6. (V2) Drag an app into the main workspace (macOS-style).

3. Phase 1 — detailed implementation

UI-first. Everything hand-coded; no ~/.rowboat storage, no IPC, no agent yet. We do mirror the eventual shapes so later phases slot in.

3.1 New files (renderer)

File Purpose
apps/renderer/src/mini-apps/types.ts MiniApp type (id, name, description, icon, accent, scope, html, data).
apps/renderer/src/mini-apps/registry.ts Hardcoded list of MiniApps for Phase 1.
apps/renderer/src/mini-apps/apps/twitter-client.ts The sample app: self-contained HTML (React+Tailwind+Babel CDN) + static data.
apps/renderer/src/components/mini-apps-view.tsx Card grid; internal selectedAppId state; renders grid or open app.
apps/renderer/src/components/mini-app-frame.tsx Sandboxed iframe (srcdoc) + window.rowboat postMessage bridge (data wired; actions stubbed → toast/log).

The bridge message protocol (host ↔ iframe), defined once and shared:

  • iframe → host: { type: 'rowboat:mini-app:ready' }, { type: 'rowboat:mini-app:action', id, scope, action, args }, { type: 'rowboat:mini-app:setState', patch }
  • host → iframe: { type: 'rowboat:mini-app:data', data }, { type: 'rowboat:mini-app:state', state }, { type: 'rowboat:mini-app:action-result', id, ok, result?, error? }

3.2 Wiring into App.tsx (mirror the bg-tasks view exactly)

Add a first-class apps view. Edit sites (all mirror an existing view):

  1. Tab path const (~L198): add const APPS_TAB_PATH = '__rowboat_mini_apps__'.
  2. Tab predicate (~L374): const isAppsTabPath = (path) => path === APPS_TAB_PATH.
  3. ViewState union (~L636): add | { type: 'apps' }.
  4. parseDeepLink (~L713): add case 'apps': return { type: 'apps' }.
  5. State flag (~L825): const [isAppsOpen, setIsAppsOpen] = useState(false).
  6. Tab title (~L1253): if (isAppsTabPath(tab.path)) return 'Mini Apps'.
  7. Tab activation → flag (~L3269, ~L3443): set isAppsOpen from tab path.
  8. Closing-tab guard (~L3376): add && !isAppsTabPath(closingTab.path).
  9. currentViewState (~L3770): if (isAppsOpen) return { type: 'apps' } (place alongside bg-tasks; add to deps array).
  10. ensureAppsFileTab (~L3853): mirror ensureBgTasksFileTab.
  11. openAppsView (~L3953): mirror openBgTasksView; reset all other flags, setIsAppsOpen(true), ensureAppsFileTab().
  12. applyViewState switch (~L4195): add case 'apps': mirroring bg-tasks.
  13. Add setIsAppsOpen(false) to every other view's reset block (the shared multi-set...(false) lines) so opening another view clears apps and closing it doesn't fall back to apps. Use targeted edits per block.
  14. Derived booleans that OR all view flags (isFullScreenChat, rightPaneAvailable, inFileView, rightPaneContext, viewOpen, keyboard guards): add || isAppsOpen / && !isAppsOpen consistently (search isBgTasksOpen).
  15. Tab-path mapping (~L4668): add : isAppsOpen ? APPS_TAB_PATH.
  16. Render branch (~L5894): add ) : isAppsOpen ? (<MiniAppsView ... />.

3.3 Sidebar entry (sidebar-content.tsx)

  • Add onOpenApps?: () => void prop (~L163) and destructure (~L412).
  • Add a SidebarMenuButton with isActive={activeNav === 'apps'} and a LayoutGrid (lucide) icon, label "Mini Apps", in the lower nav group near "Background agents" (~L830).
  • Thread activeNav === 'apps' from the parent (App.tsx passes activeNav based on isAppsOpen, mirroring how 'agents' is derived ~L5651).
  • Wire onOpenApps={openAppsView} at the SidebarContent/home call sites (~L5657, ~L5832).

3.4 The sample app (twitter-client)

Self-contained HTML string. React + ReactDOM + Babel-standalone + Tailwind, all via CDN. Renders three sections — To read / To repost / To respond (reply pre-drafted) — from data delivered via rowboat.onData. Buttons call rowboat.callAction('twitter', ...); in Phase 1 the host just toasts/logs and returns a fake ok. Static data lives next to the HTML.

Note: CDN scripts require network + allow-scripts in the sandbox. The frame uses sandbox="allow-scripts allow-popups allow-forms allow-modals"no allow-same-origin (keeps the app from reaching host cookies/storage; the bridge is the only channel). Verify CDN loads under this sandbox during testing; if blocked, fall back to bundling the libs locally as a renderer asset.

3.5 Verify

  • cd apps/x && npm run deps && npm run lint compiles clean.
  • npm run dev: "Mini Apps" appears in the sidebar; opens the card grid; clicking the Twitter card renders the app full-screen in the pane; sections populate from data; clicking a button shows the stubbed action result; tabs/back/forward and switching to other views behave like every other view.

4. Out of scope for Phase 1 (later phases)

~/.rowboat/apps/<slug>/ storage + IPC; the agent backend (reuse bg-tasks engine); real Composio execution through the bridge; per-app scope enforcement & auth prompting; generation via Copilot/Code-Mode; persistent state store; conversational editing & error recovery; the V2 workspace drag.