rowboat/apps/x/MINI_APPS_PLAN.md

308 lines
17 KiB
Markdown
Raw Normal View History

# 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 schema**`packages/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. **Main**`apps/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 → `seed`
`list` → 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-tools``composio-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 `MiniApp`s 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.