rowboat/apps/x/MINI_APPS_PLAN.md
Gagan 4a4dcb1fa0 feat(mini-apps): copilot builder — build-mini-app skill + install/data tools
Lets the copilot build a Mini App into ~/.rowboat/apps from chat.

- build-mini-app skill: intent gate (confirm when ambiguous), verify Composio
  wiring by actually calling tools before building, writer branch (Code Mode
  via code-with-agents vs Copilot), agent-backed data pipeline, install
- mini-app-install tool: write an app folder (manifest + dist/index.html)
- mini-app-set-data tool: atomic deterministic data.json write (agent returns
  content; path/write handled in code)
- serve canonical bridge shim at app://miniapp/__bridge__.js
2026-07-01 01:25:45 +05:30

17 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).

2b. Phase 2.5 — On-disk apps + app://miniapp serving (CURRENT)

Move built-in apps out of source into ~/.rowboat/apps/<id>/, served as static assets, with an optional background agent producing data.json. Sets up the copilot builder path (it will write the same folder layout). Team-agreed.

On-disk layout (one folder per app)

~/.rowboat/apps/<id>/
  manifest.json     # id, title, description, source, scope[], active, lastRun,
                    #   entry (default "dist/index.html"), agent (optional bg-task slug)
  dist/             # static assets served via app://miniapp/<id>/...
    index.html      # the app (self-contained; bridge shim inlined)
  data.json         # latest agent output (read by the host, pushed to the app)

Serving — app://miniapp/<id>/<path> (option A)

  • Extend registerAppProtocol (apps/main/src/main.ts) with a new host miniapp: map app://miniapp/<id>/<path>~/.rowboat/apps/<id>/dist/<path> (path-traversal guarded; default index.html). app:// is already registered privileged (standard/secure/fetch/cors) so apps get a real origin — remote CDNs/images and fetch work, unlike the opaque srcdoc origin.
  • The iframe loads via src="app://miniapp/<id>/index.html" (not srcDoc). Sandbox becomes allow-scripts allow-same-origin allow-popups allow-forms allow-modals allow-downloads — same-origin is its OWN app://miniapp origin, still isolated from the renderer (different host).

Data path

  • Built-in/static data is seeded to data.json. A future background agent (reuse bg-tasks engine, linked via manifest agent) overwrites it on schedule.
  • App keeps using rowboat.onData; the host now sources that data by reading ~/.rowboat/apps/<id>/data.json (via IPC) and posting it on ready. Composio still flows through the bridge RPC. (GitHub app needs no data.json — it pulls live via Composio.)

Steps

  1. Shared schemapackages/shared/src/mini-app.ts: MiniAppManifest zod + type. Export it. New IPC channels in shared/src/ipc.ts:
    • mini-apps:seed (req: {apps:[{manifest, html, data?}]}) — idempotent.
    • mini-apps:list (res: {manifests: MiniAppManifest[]}).
    • mini-apps:get-data (req {id}, res {data: unknown|null}).
  2. Mainapps/main/src/mini-apps-handler.ts: seedApps (write manifest/dist/data to ~/.rowboat/apps/<id>/ only if absent), listApps (read manifests), getAppData (read data.json). Register handlers in ipc.ts. Extend registerAppProtocol for the miniapp host.
  3. Renderer — keep MiniApp defs + buildMiniAppHtml; registry.ts adds toSeed(app) (→ {manifest, html, data}). MiniAppsView: on mount → seedlist → render cards from manifests. MiniAppFrame: take the manifest, load src=app://miniapp/<id>/<entry>, on ready fetch mini-apps:get-data and post it; RPC scope from manifest.scope.
  4. Verify: GitHub app end-to-end from disk (~/.rowboat/apps/github-radar/), then the others; confirm connect + live PRs still work.

Out of scope here (next: copilot builder, tomorrow)

Copilot writing a new app folder + an associated background agent; the agent browsing via embedded browser for social feeds; copilot verifying Composio wiring by actually calling tools before finalizing.

2c. Remaining work (after on-disk move)

Done so far: surface + runtime (Phase 1), real scoped Composio bridge (Phase 2), apps on disk served via app://miniapp (Phase 2.5).

Next — Copilot builder (demo target).

  • Copilot creates an app folder in ~/.rowboat/apps/<id>/ via the mini-apps:seed install primitive (writes manifest.json + dist/index.html).
  • Copilot must verify wiring by actually calling Composio tools and inspecting the returned data before finalizing — never speculate the shape.
  • Give generated apps the bridge contract — prefer serving a canonical shim from the protocol (e.g. app://miniapp/__bridge__.js) over inlining.

Background-agent data pipeline (deterministic).

  • Reuse the existing bg-tasks engine; link via manifest.agent.
  • The agent does NOT write files. It returns structured data validated against the app's data schema; the bg-task runner (code) atomically writes the app's data.json (temp→rename, keep last-good on failure). Path / atomicity / validation / fallback all live in code — only the content is LLM-driven.
  • Social feeds (Twitter/LinkedIn/Reddit): the agent browses via the embedded browser, curates, returns data → runner writes data.json.

Later (roadmap).

  • Living apps + breakage recovery (edit by chat; surface errors → fix-with-copilot).
  • Scale/polish: context-overload windowing; app notifications; design-consistency / component templates (team-deferred).
  • V2: drag an app into the main workspace.

2d. Mini App builder skill — spec

A Copilot skill (build-mini-app, in packages/core/src/application/assistant/ skills/) that turns a chat request into an installed app under ~/.rowboat/ apps/<id>/. Copilot orchestrates; the actual code-writing is delegated by the chat's active engine (the chip), but the on-disk artifact is identical either way.

Trigger + intent gate (like live-note/background-task signal tiers):

  • Strong (build directly): "make/build/create an app · mini app · dashboard for …", "turn this into an app".
  • Medium (CONFIRM FIRST): requests that could be a one-off answer or a recurring app — e.g. "show me my open PRs", "track competitor launches". Ask: "Want this as a Mini App you can reopen, or just a one-time answer?" Build only on yes. (Building installs a folder + maybe a bg agent + an OAuth prompt — too heavy to trigger on a casual question.)
  • Anti (do NOT build): clearly one-off lookups/questions → just answer.

Flow:

  1. Scope the app — derive id (slug), title, description, source, the Composio scope[], and whether it's agent-backed (scheduled data) or live (calls Composio on use).
  2. Verify wiring BEFORE building (mandatory, engine-agnostic) — ensure the toolkits are connected (prompt OAuth if not), then actually call the needed tools (composio-search-toolscomposio-execute-tool), inspect the REAL returned data, and derive the data schema from actual responses — never guess the shape.
  3. Pick the writer (branch):
    • Code Mode active → create the folder + a manifest skeleton, then code_agent_run with cwd = ~/.rowboat/apps/<id>/ to author dist/index.html against the verified schema + bridge contract; it can iterate/test on-device.
    • No Code Mode → Copilot writes dist/index.html itself (from the app template + bridge shim) and installs via mini-apps:seed.
  4. Bridge contract — generated app references the canonical shim (app://miniapp/__bridge__.js) and uses window.rowboat (getData/onData, isConnected/connect, searchTools/callAction).
  5. Data pipeline (if agent-backed) — create a background task (existing bg-tasks engine), set manifest.agent to its slug. The agent returns schema-validated data; the runner writes data.json deterministically (temp→rename, last-good on failure). App reads it via onData.
  6. Finalize — write manifest.json (incl. scope, agent?), ensure dist/index.html, install → app appears in the gallery (mini-apps:list). Confirm end-to-end (open it; data loads).

Infra this needs (repo side):

  • Serve the canonical bridge shim from the protocol: app://miniapp/__bridge__.js (move the shim out of per-app inlining into served infra).
  • bg-tasks runner: when a task is app-linked, validate its structured output against the app schema and write that app's data.json (deterministic write mode) instead of index.md.
  • A build-mini-app skill registered in the skills catalog.

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.