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
This commit is contained in:
Gagan 2026-06-30 00:30:29 +05:30
parent 753e3448f0
commit 7acbf67cd4
11 changed files with 1152 additions and 71 deletions

169
apps/x/MINI_APPS_PLAN.md Normal file
View file

@ -0,0 +1,169 @@
# 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 `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.

View file

@ -27,6 +27,7 @@ import { SidebarContentPanel } from '@/components/sidebar-content';
import { SuggestedTopicsView } from '@/components/suggested-topics-view'; import { SuggestedTopicsView } from '@/components/suggested-topics-view';
import { LiveNotesView } from '@/components/live-notes-view'; import { LiveNotesView } from '@/components/live-notes-view';
import { BgTasksView } from '@/components/bg-tasks-view'; import { BgTasksView } from '@/components/bg-tasks-view';
import { MiniAppsView } from '@/components/mini-apps-view';
import { EmailView } from '@/components/email-view'; import { EmailView } from '@/components/email-view';
import { WorkspaceView } from '@/components/workspace-view'; import { WorkspaceView } from '@/components/workspace-view';
import { CodingRunBlock } from '@/components/coding-run'; import { CodingRunBlock } from '@/components/coding-run';
@ -196,6 +197,7 @@ const SUGGESTED_TOPICS_TAB_PATH = '__rowboat_suggested_topics__'
const MEETINGS_TAB_PATH = '__rowboat_meetings__' const MEETINGS_TAB_PATH = '__rowboat_meetings__'
const LIVE_NOTES_TAB_PATH = '__rowboat_live_notes__' const LIVE_NOTES_TAB_PATH = '__rowboat_live_notes__'
const BG_TASKS_TAB_PATH = '__rowboat_bg_tasks__' const BG_TASKS_TAB_PATH = '__rowboat_bg_tasks__'
const APPS_TAB_PATH = '__rowboat_mini_apps__'
const EMAIL_TAB_PATH = '__rowboat_email__' const EMAIL_TAB_PATH = '__rowboat_email__'
const WORKSPACE_TAB_PATH = '__rowboat_workspace__' const WORKSPACE_TAB_PATH = '__rowboat_workspace__'
const WORKSPACE_ROOT = 'knowledge/Workspace' const WORKSPACE_ROOT = 'knowledge/Workspace'
@ -372,6 +374,7 @@ const isSuggestedTopicsTabPath = (path: string) => path === SUGGESTED_TOPICS_TAB
const isMeetingsTabPath = (path: string) => path === MEETINGS_TAB_PATH const isMeetingsTabPath = (path: string) => path === MEETINGS_TAB_PATH
const isLiveNotesTabPath = (path: string) => path === LIVE_NOTES_TAB_PATH const isLiveNotesTabPath = (path: string) => path === LIVE_NOTES_TAB_PATH
const isBgTasksTabPath = (path: string) => path === BG_TASKS_TAB_PATH const isBgTasksTabPath = (path: string) => path === BG_TASKS_TAB_PATH
const isAppsTabPath = (path: string) => path === APPS_TAB_PATH
const isEmailTabPath = (path: string) => path === EMAIL_TAB_PATH const isEmailTabPath = (path: string) => path === EMAIL_TAB_PATH
const isWorkspaceTabPath = (path: string) => path === WORKSPACE_TAB_PATH const isWorkspaceTabPath = (path: string) => path === WORKSPACE_TAB_PATH
const isKnowledgeViewTabPath = (path: string) => path === KNOWLEDGE_VIEW_TAB_PATH const isKnowledgeViewTabPath = (path: string) => path === KNOWLEDGE_VIEW_TAB_PATH
@ -634,6 +637,7 @@ type ViewState =
| { type: 'home' } | { type: 'home' }
| { type: 'code' } | { type: 'code' }
| { type: 'bg-tasks' } | { type: 'bg-tasks' }
| { type: 'apps' }
function viewStatesEqual(a: ViewState, b: ViewState): boolean { function viewStatesEqual(a: ViewState, b: ViewState): boolean {
if (a.type !== b.type) return false if (a.type !== b.type) return false
@ -712,6 +716,8 @@ function parseDeepLink(input: string): ViewState | null {
return { type: 'code' } return { type: 'code' }
case 'bg-tasks': case 'bg-tasks':
return { type: 'bg-tasks' } return { type: 'bg-tasks' }
case 'apps':
return { type: 'apps' }
default: default:
return null return null
} }
@ -823,6 +829,7 @@ function App() {
const [isMeetingsOpen, setIsMeetingsOpen] = useState(false) const [isMeetingsOpen, setIsMeetingsOpen] = useState(false)
const [isLiveNotesOpen, setIsLiveNotesOpen] = useState(false) const [isLiveNotesOpen, setIsLiveNotesOpen] = useState(false)
const [isBgTasksOpen, setIsBgTasksOpen] = useState(false) const [isBgTasksOpen, setIsBgTasksOpen] = useState(false)
const [isAppsOpen, setIsAppsOpen] = useState(false)
const [isEmailOpen, setIsEmailOpen] = useState(false) const [isEmailOpen, setIsEmailOpen] = useState(false)
const [isWorkspaceOpen, setIsWorkspaceOpen] = useState(false) const [isWorkspaceOpen, setIsWorkspaceOpen] = useState(false)
const [workspaceInitialPath, setWorkspaceInitialPath] = useState<string | null>(null) const [workspaceInitialPath, setWorkspaceInitialPath] = useState<string | null>(null)
@ -1251,6 +1258,7 @@ function App() {
if (isMeetingsTabPath(tab.path)) return 'Meetings' if (isMeetingsTabPath(tab.path)) return 'Meetings'
if (isLiveNotesTabPath(tab.path)) return 'Live notes' if (isLiveNotesTabPath(tab.path)) return 'Live notes'
if (isBgTasksTabPath(tab.path)) return 'Background tasks' if (isBgTasksTabPath(tab.path)) return 'Background tasks'
if (isAppsTabPath(tab.path)) return 'Mini Apps'
if (isEmailTabPath(tab.path)) return 'Email' if (isEmailTabPath(tab.path)) return 'Email'
if (isWorkspaceTabPath(tab.path)) return 'Workspace' if (isWorkspaceTabPath(tab.path)) return 'Workspace'
if (isKnowledgeViewTabPath(tab.path)) return 'Brain' if (isKnowledgeViewTabPath(tab.path)) return 'Brain'
@ -3214,7 +3222,7 @@ function App() {
setActiveFileTabId(existingTab.id) setActiveFileTabId(existingTab.id)
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
setSelectedPath(path) setSelectedPath(path)
return return
} }
@ -3223,7 +3231,7 @@ function App() {
setActiveFileTabId(id) setActiveFileTabId(id)
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
setSelectedPath(path) setSelectedPath(path)
}, [fileTabs, dismissBrowserOverlay]) }, [fileTabs, dismissBrowserOverlay])
@ -3242,14 +3250,14 @@ function App() {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(true) setIsGraphOpen(true)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
return return
} }
if (isSuggestedTopicsTabPath(tab.path)) { if (isSuggestedTopicsTabPath(tab.path)) {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(true) setIsSuggestedTopicsOpen(true)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
return return
} }
if (isLiveNotesTabPath(tab.path)) { if (isLiveNotesTabPath(tab.path)) {
@ -3262,7 +3270,7 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setIsLiveNotesOpen(true) setIsLiveNotesOpen(true)
return return
} }
@ -3276,10 +3284,25 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setIsBgTasksOpen(true) setIsBgTasksOpen(true)
return return
} }
if (isAppsTabPath(tab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false)
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false)
setIsHomeOpen(false)
setIsAppsOpen(true)
return
}
if (isMeetingsTabPath(tab.path)) { if (isMeetingsTabPath(tab.path)) {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
@ -3291,7 +3314,7 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
return return
} }
if (isEmailTabPath(tab.path)) { if (isEmailTabPath(tab.path)) {
@ -3304,7 +3327,7 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setIsEmailOpen(true) setIsEmailOpen(true)
return return
} }
@ -3318,7 +3341,7 @@ function App() {
setIsEmailOpen(false) setIsEmailOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setIsWorkspaceOpen(true) setIsWorkspaceOpen(true)
return return
} }
@ -3332,7 +3355,7 @@ function App() {
setIsEmailOpen(false) setIsEmailOpen(false)
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setIsKnowledgeViewOpen(true) setIsKnowledgeViewOpen(true)
return return
} }
@ -3346,7 +3369,7 @@ function App() {
setIsEmailOpen(false) setIsEmailOpen(false)
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(true); setIsHomeOpen(false) setIsChatHistoryOpen(true); setIsHomeOpen(false); setIsAppsOpen(false)
return return
} }
if (isHomeTabPath(tab.path)) { if (isHomeTabPath(tab.path)) {
@ -3354,7 +3377,7 @@ function App() {
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
setIsHomeOpen(true) setIsHomeOpen(true); setIsAppsOpen(false)
return return
} }
if (isCodeTabPath(tab.path)) { if (isCodeTabPath(tab.path)) {
@ -3362,18 +3385,18 @@ function App() {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
return return
} }
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
setSelectedPath(tab.path) setSelectedPath(tab.path)
}, [fileTabs, isRightPaneMaximized, dismissBrowserOverlay]) }, [fileTabs, isRightPaneMaximized, dismissBrowserOverlay])
const closeFileTab = useCallback((tabId: string) => { const closeFileTab = useCallback((tabId: string) => {
const closingTab = fileTabs.find(t => t.id === tabId) const closingTab = fileTabs.find(t => t.id === tabId)
if (closingTab && !isGraphTabPath(closingTab.path) && !isSuggestedTopicsTabPath(closingTab.path) && !isLiveNotesTabPath(closingTab.path) && !isBgTasksTabPath(closingTab.path) && !isEmailTabPath(closingTab.path) && !isWorkspaceTabPath(closingTab.path) && !isKnowledgeViewTabPath(closingTab.path) && !isChatHistoryTabPath(closingTab.path) && !isHomeTabPath(closingTab.path) && !isCodeTabPath(closingTab.path) && !isBaseFilePath(closingTab.path)) { if (closingTab && !isGraphTabPath(closingTab.path) && !isSuggestedTopicsTabPath(closingTab.path) && !isLiveNotesTabPath(closingTab.path) && !isBgTasksTabPath(closingTab.path) && !isAppsTabPath(closingTab.path) && !isEmailTabPath(closingTab.path) && !isWorkspaceTabPath(closingTab.path) && !isKnowledgeViewTabPath(closingTab.path) && !isChatHistoryTabPath(closingTab.path) && !isHomeTabPath(closingTab.path) && !isCodeTabPath(closingTab.path) && !isBaseFilePath(closingTab.path)) {
removeEditorCacheForPath(closingTab.path) removeEditorCacheForPath(closingTab.path)
initialContentByPathRef.current.delete(closingTab.path) initialContentByPathRef.current.delete(closingTab.path)
untitledRenameReadyPathsRef.current.delete(closingTab.path) untitledRenameReadyPathsRef.current.delete(closingTab.path)
@ -3396,7 +3419,7 @@ function App() {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
return [] return []
} }
const idx = prev.findIndex(t => t.id === tabId) const idx = prev.findIndex(t => t.id === tabId)
@ -3410,12 +3433,12 @@ function App() {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(true) setIsGraphOpen(true)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
} else if (isSuggestedTopicsTabPath(newActiveTab.path)) { } else if (isSuggestedTopicsTabPath(newActiveTab.path)) {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(true) setIsSuggestedTopicsOpen(true)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
} else if (isMeetingsTabPath(newActiveTab.path)) { } else if (isMeetingsTabPath(newActiveTab.path)) {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
@ -3427,7 +3450,7 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
} else if (isLiveNotesTabPath(newActiveTab.path)) { } else if (isLiveNotesTabPath(newActiveTab.path)) {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
@ -3438,7 +3461,7 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setIsLiveNotesOpen(true) setIsLiveNotesOpen(true)
} else if (isBgTasksTabPath(newActiveTab.path)) { } else if (isBgTasksTabPath(newActiveTab.path)) {
setSelectedPath(null) setSelectedPath(null)
@ -3451,7 +3474,20 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
} else if (isAppsTabPath(newActiveTab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false)
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false)
setIsHomeOpen(false)
setIsAppsOpen(true)
} else if (isEmailTabPath(newActiveTab.path)) { } else if (isEmailTabPath(newActiveTab.path)) {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
@ -3462,7 +3498,7 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setIsEmailOpen(true) setIsEmailOpen(true)
} else if (isWorkspaceTabPath(newActiveTab.path)) { } else if (isWorkspaceTabPath(newActiveTab.path)) {
setSelectedPath(null) setSelectedPath(null)
@ -3474,7 +3510,7 @@ function App() {
setIsEmailOpen(false) setIsEmailOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setIsWorkspaceOpen(true) setIsWorkspaceOpen(true)
} else if (isKnowledgeViewTabPath(newActiveTab.path)) { } else if (isKnowledgeViewTabPath(newActiveTab.path)) {
setSelectedPath(null) setSelectedPath(null)
@ -3486,7 +3522,7 @@ function App() {
setIsEmailOpen(false) setIsEmailOpen(false)
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setIsKnowledgeViewOpen(true) setIsKnowledgeViewOpen(true)
} else if (isChatHistoryTabPath(newActiveTab.path)) { } else if (isChatHistoryTabPath(newActiveTab.path)) {
setSelectedPath(null) setSelectedPath(null)
@ -3498,17 +3534,17 @@ function App() {
setIsEmailOpen(false) setIsEmailOpen(false)
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(true); setIsHomeOpen(false) setIsChatHistoryOpen(true); setIsHomeOpen(false); setIsAppsOpen(false)
} else if (isHomeTabPath(newActiveTab.path)) { } else if (isHomeTabPath(newActiveTab.path)) {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
setIsHomeOpen(true) setIsHomeOpen(true); setIsAppsOpen(false)
} else { } else {
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
setSelectedPath(newActiveTab.path) setSelectedPath(newActiveTab.path)
} }
} }
@ -3530,7 +3566,7 @@ function App() {
dismissBrowserOverlay() dismissBrowserOverlay()
handleNewChat() handleNewChat()
// Left-pane "new chat" should always open full chat view. // Left-pane "new chat" should always open full chat view.
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) { if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) {
setExpandedFrom({ setExpandedFrom({
path: selectedPath, path: selectedPath,
graph: isGraphOpen, graph: isGraphOpen,
@ -3547,8 +3583,8 @@ function App() {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
}, [dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, isHomeOpen]) }, [dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isAppsOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, isHomeOpen])
// Sidebar variant: reset the chat in place without leaving file/graph context. // Sidebar variant: reset the chat in place without leaving file/graph context.
const handleNewChatTabInSidebar = useCallback(() => { const handleNewChatTabInSidebar = useCallback(() => {
@ -3680,7 +3716,7 @@ function App() {
const handleOpenFullScreenChat = useCallback(() => { const handleOpenFullScreenChat = useCallback(() => {
// Remember where we came from so the close button can return // Remember where we came from so the close button can return
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) { if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) {
setExpandedFrom({ setExpandedFrom({
path: selectedPath, path: selectedPath,
graph: isGraphOpen, graph: isGraphOpen,
@ -3696,8 +3732,8 @@ function App() {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
}, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, dismissBrowserOverlay]) }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isAppsOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, dismissBrowserOverlay])
const handleCloseFullScreenChat = useCallback((): boolean => { const handleCloseFullScreenChat = useCallback((): boolean => {
let restored = false let restored = false
@ -3706,11 +3742,11 @@ function App() {
if (expandedFrom.graph) { if (expandedFrom.graph) {
setIsGraphOpen(true) setIsGraphOpen(true)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
} else if (expandedFrom.suggestedTopics) { } else if (expandedFrom.suggestedTopics) {
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(true) setIsSuggestedTopicsOpen(true)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
} else if (expandedFrom.meetings) { } else if (expandedFrom.meetings) {
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
@ -3742,7 +3778,7 @@ function App() {
} else if (expandedFrom.path) { } else if (expandedFrom.path) {
setIsGraphOpen(false) setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
setSelectedPath(expandedFrom.path) setSelectedPath(expandedFrom.path)
} else { } else {
// expandedFrom was captured from a view this restorer doesn't track // expandedFrom was captured from a view this restorer doesn't track
@ -3768,10 +3804,11 @@ function App() {
if (isHomeOpen) return { type: 'home' } if (isHomeOpen) return { type: 'home' }
if (isCodeOpen) return { type: 'code' } if (isCodeOpen) return { type: 'code' }
if (isBgTasksOpen) return { type: 'bg-tasks' } if (isBgTasksOpen) return { type: 'bg-tasks' }
if (isAppsOpen) return { type: 'apps' }
if (selectedPath) return { type: 'file', path: selectedPath } if (selectedPath) return { type: 'file', path: selectedPath }
if (isGraphOpen) return { type: 'graph' } if (isGraphOpen) return { type: 'graph' }
return { type: 'chat', runId } return { type: 'chat', runId }
}, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, isWorkspaceOpen, isKnowledgeViewOpen, knowledgeViewFolderPath, knowledgeViewMode, isChatHistoryOpen, isHomeOpen, isCodeOpen, workspaceInitialPath, runId]) }, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isAppsOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, isWorkspaceOpen, isKnowledgeViewOpen, knowledgeViewFolderPath, knowledgeViewMode, isChatHistoryOpen, isHomeOpen, isCodeOpen, workspaceInitialPath, runId])
const appendUnique = useCallback((stack: ViewState[], entry: ViewState) => { const appendUnique = useCallback((stack: ViewState[], entry: ViewState) => {
const last = stack[stack.length - 1] const last = stack[stack.length - 1]
@ -3861,6 +3898,17 @@ function App() {
setActiveFileTabId(id) setActiveFileTabId(id)
}, [fileTabs]) }, [fileTabs])
const ensureAppsFileTab = useCallback(() => {
const existing = fileTabs.find((tab) => isAppsTabPath(tab.path))
if (existing) {
setActiveFileTabId(existing.id)
return
}
const id = newFileTabId()
setFileTabs((prev) => [...prev, { id, path: APPS_TAB_PATH }])
setActiveFileTabId(id)
}, [fileTabs])
const ensureEmailFileTab = useCallback(() => { const ensureEmailFileTab = useCallback(() => {
const existing = fileTabs.find((tab) => isEmailTabPath(tab.path)) const existing = fileTabs.find((tab) => isEmailTabPath(tab.path))
if (existing) { if (existing) {
@ -3938,7 +3986,7 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setSelectedBackgroundTask(null) setSelectedBackgroundTask(null)
setExpandedFrom(null) setExpandedFrom(null)
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
@ -3955,7 +4003,7 @@ function App() {
setIsGraphOpen(false) setIsGraphOpen(false)
setIsBrowserOpen(false) setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
setSelectedBackgroundTask(null) setSelectedBackgroundTask(null)
setExpandedFrom(null) setExpandedFrom(null)
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
@ -3963,6 +4011,19 @@ function App() {
ensureBgTasksFileTab() ensureBgTasksFileTab()
}, [ensureBgTasksFileTab]) }, [ensureBgTasksFileTab])
const openAppsView = useCallback(() => {
setSelectedPath(null)
setIsGraphOpen(false)
setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
setSelectedBackgroundTask(null)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
setIsAppsOpen(true)
ensureAppsFileTab()
}, [ensureAppsFileTab])
const openMeetingsView = useCallback(() => { const openMeetingsView = useCallback(() => {
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
@ -3975,7 +4036,7 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setSelectedBackgroundTask(null) setSelectedBackgroundTask(null)
setExpandedFrom(null) setExpandedFrom(null)
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
@ -3987,7 +4048,7 @@ function App() {
setIsGraphOpen(false) setIsGraphOpen(false)
setIsBrowserOpen(false) setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
setSelectedBackgroundTask(null) setSelectedBackgroundTask(null)
setExpandedFrom(null) setExpandedFrom(null)
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
@ -4003,7 +4064,7 @@ function App() {
// visible in the middle pane. // visible in the middle pane.
setIsBrowserOpen(false) setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
setExpandedFrom(null) setExpandedFrom(null)
// Preserve split vs knowledge-max mode when navigating knowledge files. // Preserve split vs knowledge-max mode when navigating knowledge files.
// Only exit chat-only maximize, because that would hide the selected file. // Only exit chat-only maximize, because that would hide the selected file.
@ -4018,7 +4079,7 @@ function App() {
setSelectedPath(null) setSelectedPath(null)
setIsBrowserOpen(false) setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
setExpandedFrom(null) setExpandedFrom(null)
setIsGraphOpen(true) setIsGraphOpen(true)
ensureGraphFileTab() ensureGraphFileTab()
@ -4031,7 +4092,7 @@ function App() {
setIsGraphOpen(false) setIsGraphOpen(false)
setIsBrowserOpen(false) setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
setExpandedFrom(null) setExpandedFrom(null)
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
setSelectedBackgroundTask(view.name) setSelectedBackgroundTask(view.name)
@ -4044,7 +4105,7 @@ function App() {
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
setSelectedBackgroundTask(null) setSelectedBackgroundTask(null)
setIsSuggestedTopicsOpen(true) setIsSuggestedTopicsOpen(true)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
ensureSuggestedTopicsFileTab() ensureSuggestedTopicsFileTab()
return return
case 'meetings': case 'meetings':
@ -4062,7 +4123,7 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
ensureMeetingsFileTab() ensureMeetingsFileTab()
return return
case 'live-notes': case 'live-notes':
@ -4079,7 +4140,7 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setIsLiveNotesOpen(true) setIsLiveNotesOpen(true)
ensureLiveNotesFileTab() ensureLiveNotesFileTab()
return return
@ -4098,7 +4159,7 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
// Deep links (e.g. a new-email notification) carry the thread to open; // Deep links (e.g. a new-email notification) carry the thread to open;
// bump the version so EmailView re-selects it even if email is already open. // bump the version so EmailView re-selects it even if email is already open.
if (view.threadId) { if (view.threadId) {
@ -4122,7 +4183,7 @@ function App() {
setIsWorkspaceOpen(true) setIsWorkspaceOpen(true)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
setWorkspaceInitialPath(view.path ?? null) setWorkspaceInitialPath(view.path ?? null)
ensureWorkspaceFileTab() ensureWorkspaceFileTab()
return return
@ -4143,7 +4204,7 @@ function App() {
setKnowledgeViewMode(view.mode ?? (view.folderPath ? 'files' : 'graph')) setKnowledgeViewMode(view.mode ?? (view.folderPath ? 'files' : 'graph'))
setKnowledgeViewFolderPath(view.folderPath ?? null) setKnowledgeViewFolderPath(view.folderPath ?? null)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(false) setIsHomeOpen(false); setIsAppsOpen(false)
ensureKnowledgeViewFileTab() ensureKnowledgeViewFileTab()
return return
case 'chat-history': case 'chat-history':
@ -4160,7 +4221,7 @@ function App() {
setIsEmailOpen(false) setIsEmailOpen(false)
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(true); setIsHomeOpen(false) setIsChatHistoryOpen(true); setIsHomeOpen(false); setIsAppsOpen(false)
ensureChatHistoryFileTab() ensureChatHistoryFileTab()
return return
case 'home': case 'home':
@ -4178,7 +4239,7 @@ function App() {
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false) setIsChatHistoryOpen(false)
setIsHomeOpen(true) setIsHomeOpen(true); setIsAppsOpen(false)
ensureHomeFileTab() ensureHomeFileTab()
return return
case 'code': case 'code':
@ -4189,7 +4250,7 @@ function App() {
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
setSelectedBackgroundTask(null) setSelectedBackgroundTask(null)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
ensureCodeFileTab() ensureCodeFileTab()
return return
case 'bg-tasks': case 'bg-tasks':
@ -4200,10 +4261,22 @@ function App() {
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
setSelectedBackgroundTask(null) setSelectedBackgroundTask(null)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
setIsBgTasksOpen(true) setIsBgTasksOpen(true)
ensureBgTasksFileTab() ensureBgTasksFileTab()
return return
case 'apps':
setSelectedPath(null)
setIsGraphOpen(false)
setIsBrowserOpen(false)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
setSelectedBackgroundTask(null)
setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
setIsAppsOpen(true)
ensureAppsFileTab()
return
case 'chat': case 'chat':
setSelectedPath(null) setSelectedPath(null)
setIsGraphOpen(false) setIsGraphOpen(false)
@ -4212,7 +4285,7 @@ function App() {
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
setSelectedBackgroundTask(null) setSelectedBackgroundTask(null)
setIsSuggestedTopicsOpen(false) setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
if (view.runId) { if (view.runId) {
const targetRunId = view.runId const targetRunId = view.runId
// Bind the loaded run to a chat tab so its title (derived from // Bind the loaded run to a chat tab so its title (derived from
@ -4232,7 +4305,7 @@ function App() {
} }
return return
} }
}, [ensureEmailFileTab, ensureMeetingsFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, ensureWorkspaceFileTab, ensureKnowledgeViewFileTab, ensureChatHistoryFileTab, ensureHomeFileTab, ensureCodeFileTab, ensureBgTasksFileTab, handleNewChat, isRightPaneMaximized, loadRun]) }, [ensureEmailFileTab, ensureMeetingsFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, ensureWorkspaceFileTab, ensureKnowledgeViewFileTab, ensureChatHistoryFileTab, ensureHomeFileTab, ensureCodeFileTab, ensureBgTasksFileTab, ensureAppsFileTab, handleNewChat, isRightPaneMaximized, loadRun])
const navigateToView = useCallback(async (nextView: ViewState) => { const navigateToView = useCallback(async (nextView: ViewState) => {
const current = currentViewState const current = currentViewState
@ -4566,7 +4639,7 @@ function App() {
}, []) }, [])
// Keyboard shortcut: Ctrl+L to toggle main chat view // Keyboard shortcut: Ctrl+L to toggle main chat view
const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isHomeOpen && !isCodeOpen && !selectedBackgroundTask && !isBrowserOpen const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isHomeOpen && !isCodeOpen && !selectedBackgroundTask && !isBrowserOpen
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'l') { if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
@ -4651,11 +4724,11 @@ function App() {
const handleTabKeyDown = (e: KeyboardEvent) => { const handleTabKeyDown = (e: KeyboardEvent) => {
const mod = e.metaKey || e.ctrlKey const mod = e.metaKey || e.ctrlKey
if (!mod) return if (!mod) return
const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) && isChatSidebarOpen) const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) && isChatSidebarOpen)
const targetPane: ShortcutPane = rightPaneAvailable const targetPane: ShortcutPane = rightPaneAvailable
? (isRightPaneMaximized ? 'right' : activeShortcutPane) ? (isRightPaneMaximized ? 'right' : activeShortcutPane)
: 'left' : 'left'
const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen)
const selectedKnowledgePath = isGraphOpen const selectedKnowledgePath = isGraphOpen
? GRAPH_TAB_PATH ? GRAPH_TAB_PATH
: isSuggestedTopicsOpen : isSuggestedTopicsOpen
@ -4666,6 +4739,8 @@ function App() {
? LIVE_NOTES_TAB_PATH ? LIVE_NOTES_TAB_PATH
: isBgTasksOpen : isBgTasksOpen
? BG_TASKS_TAB_PATH ? BG_TASKS_TAB_PATH
: isAppsOpen
? APPS_TAB_PATH
: isEmailOpen : isEmailOpen
? EMAIL_TAB_PATH ? EMAIL_TAB_PATH
: isWorkspaceOpen : isWorkspaceOpen
@ -4749,7 +4824,7 @@ function App() {
} }
document.addEventListener('keydown', handleTabKeyDown) document.addEventListener('keydown', handleTabKeyDown)
return () => document.removeEventListener('keydown', handleTabKeyDown) return () => document.removeEventListener('keydown', handleTabKeyDown)
}, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab]) }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isAppsOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
const toggleExpand = (path: string, kind: 'file' | 'dir') => { const toggleExpand = (path: string, kind: 'file' | 'dir') => {
if (kind === 'file') { if (kind === 'file') {
@ -4774,7 +4849,7 @@ function App() {
}), }),
}, },
})) }))
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) { if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
setIsChatSidebarOpen(false) setIsChatSidebarOpen(false)
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
} }
@ -4913,21 +4988,21 @@ function App() {
}, },
openGraph: () => { openGraph: () => {
// From chat-only landing state, open graph directly in full knowledge view. // From chat-only landing state, open graph directly in full knowledge view.
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) { if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
setIsChatSidebarOpen(false) setIsChatSidebarOpen(false)
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
} }
void navigateToView({ type: 'graph' }) void navigateToView({ type: 'graph' })
}, },
openBases: () => { openBases: () => {
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) { if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
setIsChatSidebarOpen(false) setIsChatSidebarOpen(false)
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
} }
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH }) void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
}, },
openWorkspaceAt: (path?: string) => { openWorkspaceAt: (path?: string) => {
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) { if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
setIsChatSidebarOpen(false) setIsChatSidebarOpen(false)
setIsRightPaneMaximized(false) setIsRightPaneMaximized(false)
} }
@ -5597,7 +5672,7 @@ function App() {
const selectedTask = selectedBackgroundTask const selectedTask = selectedBackgroundTask
? backgroundTasks.find(t => t.name === selectedBackgroundTask) ? backgroundTasks.find(t => t.name === selectedBackgroundTask)
: null : null
const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isCodeOpen || isBrowserOpen) const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isCodeOpen || isBrowserOpen)
const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized
const shouldCollapseLeftPane = isRightPaneOnlyMode const shouldCollapseLeftPane = isRightPaneOnlyMode
const nonChatPaneStyle = React.useMemo<React.CSSProperties>(() => { const nonChatPaneStyle = React.useMemo<React.CSSProperties>(() => {
@ -5645,7 +5720,7 @@ function App() {
return ( return (
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<SidebarSectionProvider defaultSection="tasks" onSectionChange={(section) => { <SidebarSectionProvider defaultSection="tasks" onSectionChange={(section) => {
if (section === 'knowledge' && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isHomeOpen) { if (section === 'knowledge' && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isHomeOpen) {
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH }) void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
} }
}}> }}>
@ -5668,6 +5743,7 @@ function App() {
: isCodeOpen ? 'code' : isCodeOpen ? 'code'
: (isKnowledgeViewOpen || isGraphOpen || (selectedPath != null && selectedPath.startsWith('knowledge/'))) ? 'knowledge' : (isKnowledgeViewOpen || isGraphOpen || (selectedPath != null && selectedPath.startsWith('knowledge/'))) ? 'knowledge'
: isBgTasksOpen ? 'agents' : isBgTasksOpen ? 'agents'
: isAppsOpen ? 'apps'
: isWorkspaceOpen ? 'workspaces' : isWorkspaceOpen ? 'workspaces'
: null : null
} }
@ -5675,6 +5751,7 @@ function App() {
onOpenCode={openCodeView} onOpenCode={openCodeView}
onOpenBgTasks={() => { setBgTaskInitialSlug(null); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }} onOpenBgTasks={() => { setBgTaskInitialSlug(null); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }}
onOpenAgent={(slug) => { setBgTaskInitialSlug(slug); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }} onOpenAgent={(slug) => { setBgTaskInitialSlug(slug); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }}
onOpenApps={openAppsView}
recentRuns={runs} recentRuns={runs}
onOpenRun={(rid) => void navigateToView({ type: 'chat', runId: rid })} onOpenRun={(rid) => void navigateToView({ type: 'chat', runId: rid })}
onOpenChatHistory={() => void navigateToView({ type: 'chat-history' })} onOpenChatHistory={() => void navigateToView({ type: 'chat-history' })}
@ -5707,7 +5784,7 @@ function App() {
canNavigateForward={canNavigateForward} canNavigateForward={canNavigateForward}
collapsedLeftPaddingPx={collapsedLeftPaddingPx} collapsedLeftPaddingPx={collapsedLeftPaddingPx}
> >
{(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isCodeOpen) && fileTabs.length >= 1 ? ( {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isCodeOpen) && fileTabs.length >= 1 ? (
<TabBar <TabBar
tabs={fileTabs} tabs={fileTabs}
activeTabId={activeFileTabId ?? ''} activeTabId={activeFileTabId ?? ''}
@ -5715,7 +5792,7 @@ function App() {
getTabId={(t) => t.id} getTabId={(t) => t.id}
onSwitchTab={switchFileTab} onSwitchTab={switchFileTab}
onCloseTab={closeFileTab} onCloseTab={closeFileTab}
allowSingleTabClose={fileTabs.length === 1 && (isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isCodeOpen || (selectedPath != null && isBaseFilePath(selectedPath)))} allowSingleTabClose={fileTabs.length === 1 && (isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isCodeOpen || (selectedPath != null && isBaseFilePath(selectedPath)))}
/> />
) : isFullScreenChat ? ( ) : isFullScreenChat ? (
<ChatHeader <ChatHeader
@ -5780,7 +5857,7 @@ function App() {
<TooltipContent side="bottom">Version history</TooltipContent> <TooltipContent side="bottom">Version history</TooltipContent>
</Tooltip> </Tooltip>
)} )}
{!isFullScreenChat && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isCodeOpen && !selectedTask && !isBrowserOpen && ( {!isFullScreenChat && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isCodeOpen && !selectedTask && !isBrowserOpen && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
@ -5800,7 +5877,7 @@ function App() {
a freshly-mounted no-drag button inside the drag-region header a freshly-mounted no-drag button inside the drag-region header
otherwise has its first click swallowed by the window drag. */} otherwise has its first click swallowed by the window drag. */}
{(() => { {(() => {
const viewOpen = selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen const viewOpen = selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen
const action = isFullScreenChat const action = isFullScreenChat
? { onClick: pushChatToSidePane, icon: <ArrowRight className="size-5" />, label: 'Dock chat to side pane' } ? { onClick: pushChatToSidePane, icon: <ArrowRight className="size-5" />, label: 'Dock chat to side pane' }
: (viewOpen && !isChatSidebarOpen) : (viewOpen && !isChatSidebarOpen)
@ -5904,6 +5981,10 @@ function App() {
}} }}
/> />
</div> </div>
) : isAppsOpen ? (
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
<MiniAppsView />
</div>
) : isEmailOpen ? ( ) : isEmailOpen ? (
<div className="flex-1 min-h-0 flex flex-col overflow-hidden"> <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
<EmailView initialThreadId={emailInitialThreadId} threadIdVersion={emailThreadIdVersion} /> <EmailView initialThreadId={emailInitialThreadId} threadIdVersion={emailThreadIdVersion} />

View file

@ -0,0 +1,96 @@
import { useEffect, useRef } from 'react'
import type { MiniApp, MiniAppOutboundMessage } from '@/mini-apps/types'
import { MINI_APP_MESSAGE } from '@/mini-apps/types'
// Host side of the Mini App bridge. Renders the app's self-contained HTML in a
// sandboxed iframe and answers the postMessage protocol in mini-apps/types.ts.
//
// Phase 1: data is delivered from the static app.data, per-app state lives in
// memory only, and callAction is stubbed (returns a friendly demo result). Later
// phases replace the action/state handlers with real IPC to the agent + Composio.
// Sandbox intentionally omits allow-same-origin: the app gets an opaque origin so
// it cannot reach host cookies/storage. The bridge is the only channel out.
const SANDBOX = 'allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals allow-downloads'
// Phase 1 stub: pretend the Composio action succeeded.
function stubActionResult(action: string): { message: string } {
switch (action) {
case 'repost': return { message: 'Reposted (demo)' }
case 'reply': return { message: 'Reply sent (demo)' }
case 'mark_read': return { message: 'Dismissed' }
default: return { message: action + ' done (demo)' }
}
}
export function MiniAppFrame({ app }: { app: MiniApp }) {
const iframeRef = useRef<HTMLIFrameElement | null>(null)
// In-memory per-app state for Phase 1 (resets when the frame unmounts).
const stateRef = useRef<Record<string, unknown>>({})
useEffect(() => {
// Reset state when switching apps.
stateRef.current = {}
function postToFrame(message: unknown) {
iframeRef.current?.contentWindow?.postMessage(message, '*')
}
function handleMessage(event: MessageEvent) {
const frameWindow = iframeRef.current?.contentWindow
if (!frameWindow || event.source !== frameWindow) return
const msg = event.data as MiniAppOutboundMessage
if (!msg || typeof msg !== 'object') return
switch (msg.type) {
case MINI_APP_MESSAGE.ready: {
postToFrame({ type: MINI_APP_MESSAGE.data, data: app.data })
postToFrame({ type: MINI_APP_MESSAGE.state, state: stateRef.current })
break
}
case MINI_APP_MESSAGE.action: {
// Phase 1: stubbed. (Phase 2 enforces app.scope and calls Composio.)
const allowed = app.scope.includes(msg.scope)
if (!allowed) {
postToFrame({
type: MINI_APP_MESSAGE.actionResult,
id: msg.id,
ok: false,
error: 'Action scope "' + msg.scope + '" not granted to this app',
})
break
}
// Small delay so the UI's busy state is visible.
window.setTimeout(() => {
postToFrame({
type: MINI_APP_MESSAGE.actionResult,
id: msg.id,
ok: true,
result: stubActionResult(msg.action),
})
}, 350)
break
}
case MINI_APP_MESSAGE.setState: {
stateRef.current = { ...stateRef.current, ...(msg.patch as Record<string, unknown>) }
postToFrame({ type: MINI_APP_MESSAGE.state, state: stateRef.current })
break
}
}
}
window.addEventListener('message', handleMessage)
return () => window.removeEventListener('message', handleMessage)
}, [app])
return (
<iframe
ref={iframeRef}
title={app.name}
srcDoc={app.html}
className="h-full w-full border-0 bg-neutral-950"
sandbox={SANDBOX}
/>
)
}

View file

@ -0,0 +1,202 @@
import { useState } from 'react'
import { ArrowLeft, Plus } from 'lucide-react'
import { MINI_APPS, getMiniApp } from '@/mini-apps/registry'
import type { MiniApp } from '@/mini-apps/types'
import { MiniAppFrame } from '@/components/mini-app-frame'
// The "Mini Apps" section: a gallery of premium product tiles; click one to open
// the app full-screen. Each card's accent theme and decorative pattern are
// derived deterministically from the app id, so identity comes from colour +
// typography rather than an icon.
type Theme = { accent: string; glow: string }
const THEMES: Theme[] = [
{ accent: '#7C5CFF', glow: 'rgba(124,92,255,0.45)' }, // Indigo
{ accent: '#4F9CFF', glow: 'rgba(79,156,255,0.45)' }, // Ocean
{ accent: '#22C55E', glow: 'rgba(34,197,94,0.40)' }, // Emerald
{ accent: '#F59E0B', glow: 'rgba(245,158,11,0.42)' }, // Amber
{ accent: '#EC4899', glow: 'rgba(236,72,153,0.42)' }, // Rose
{ accent: '#14B8A6', glow: 'rgba(20,184,166,0.40)' }, // Teal
]
const PATTERNS = ['dots', 'grid', 'diagonal', 'radial', 'waves', 'mesh']
function hash(s: string): number {
let h = 0
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0
return Math.abs(h)
}
const themeFor = (id: string): Theme => THEMES[hash(id) % THEMES.length]
const patternFor = (id: string): string => PATTERNS[hash(id + '·pat') % PATTERNS.length]
// Card styling lives here (precise gradients/glows/patterns are awkward in
// Tailwind tokens). Injected once; per-card accent is passed via CSS variables.
const CARD_CSS = `
/* Light is the baseline; .dark (set on <html>) overrides the surface tokens.
The accent system (badge/pill/glow/pattern via --accent) is identical in both. */
.ma-page {
--ma-bg:#f6f6f7;
--ma-card-from:#ffffff; --ma-card-to:#fbfbfc;
--ma-card-hover-from:#ffffff; --ma-card-hover-to:#f5f5f7;
--ma-border:rgba(0,0,0,0.08); --ma-border-hover:rgba(0,0,0,0.14);
--ma-shadow:0 1px 2px rgba(0,0,0,0.06);
--ma-title:#101013; --ma-desc:rgba(0,0,0,0.62);
--ma-h1:#101013; --ma-sub:rgba(0,0,0,0.5); --ma-lastrun:rgba(0,0,0,0.42);
--ma-off-bg:rgba(0,0,0,0.05); --ma-off-fg:rgba(0,0,0,0.5); --ma-off-dot:rgba(0,0,0,0.4);
--ma-new-border:rgba(0,0,0,0.14); --ma-new-title:rgba(0,0,0,0.6); --ma-new-hint:rgba(0,0,0,0.4);
--ma-pat-opacity:0.16; --ma-glow-opacity:0.22; --ma-glow-hover-opacity:0.30;
--ma-badge-mix:22%; --ma-pill-mix:18%;
height:100%; overflow:auto; background:var(--ma-bg);
}
.dark .ma-page {
--ma-bg:#0c0c0e;
--ma-card-from:#17171B; --ma-card-to:#111114;
--ma-card-hover-from:#19191e; --ma-card-hover-to:#121215;
--ma-border:rgba(255,255,255,0.06); --ma-border-hover:rgba(255,255,255,0.09);
--ma-shadow:0 1px 2px rgba(0,0,0,0.18);
--ma-title:#f5f5f5; --ma-desc:rgba(255,255,255,0.68);
--ma-h1:#f5f5f5; --ma-sub:rgba(255,255,255,0.55); --ma-lastrun:rgba(255,255,255,0.4);
--ma-off-bg:rgba(255,255,255,0.06); --ma-off-fg:rgba(255,255,255,0.5); --ma-off-dot:rgba(255,255,255,0.45);
--ma-new-border:rgba(255,255,255,0.12); --ma-new-title:rgba(255,255,255,0.6); --ma-new-hint:rgba(255,255,255,0.38);
--ma-pat-opacity:0.07; --ma-glow-opacity:0.10; --ma-glow-hover-opacity:0.13;
--ma-badge-mix:15%; --ma-pill-mix:13%;
}
.ma-inner { max-width:1080px; margin:0 auto; padding:32px 28px 48px; }
.ma-h1 { font-size:24px; font-weight:600; letter-spacing:-0.02em; color:var(--ma-h1); margin:0 0 4px; }
.ma-sub { font-size:14px; color:var(--ma-sub); margin:0 0 28px; }
.ma-grid { display:grid; grid-template-columns:repeat(3, 1fr); gap:28px; }
@media (max-width:1040px) { .ma-grid { grid-template-columns:repeat(2, 1fr); } }
@media (max-width:700px) { .ma-grid { grid-template-columns:1fr; } }
.ma-card {
position:relative; min-height:252px; border-radius:20px;
border:1px solid var(--ma-border);
background:linear-gradient(160deg, var(--ma-card-from) 0%, var(--ma-card-to) 100%);
padding:22px; text-align:left; cursor:pointer; overflow:hidden;
display:flex; flex-direction:column; isolation:isolate;
box-shadow: var(--ma-shadow), 0 6px 18px -20px var(--glow);
transition: box-shadow .22s ease, border-color .22s ease, background .22s ease;
}
.ma-card:hover {
border-color: var(--ma-border-hover);
background:linear-gradient(160deg, var(--ma-card-hover-from) 0%, var(--ma-card-hover-to) 100%);
}
/* decorative pattern layer (accent-tinted, very low opacity) */
.ma-card::before {
content:''; position:absolute; inset:0; z-index:-1; opacity:var(--ma-pat-opacity); pointer-events:none;
}
/* ambient glow blob, top-right */
.ma-card::after {
content:''; position:absolute; top:-45%; right:-25%; width:75%; height:75%; z-index:-1;
background: radial-gradient(circle, var(--accent) 0%, transparent 70%);
opacity:var(--ma-glow-opacity); filter: blur(18px); pointer-events:none; transition: opacity .22s ease;
}
.ma-card:hover::after { opacity:var(--ma-glow-hover-opacity); }
.ma-pat-dots::before { background-image: radial-gradient(var(--accent) 1px, transparent 1.4px); background-size:16px 16px; }
.ma-pat-grid::before { background-image: linear-gradient(var(--accent) 1px, transparent 1px), linear-gradient(90deg, var(--accent) 1px, transparent 1px); background-size:26px 26px; }
.ma-pat-diagonal::before { background-image: repeating-linear-gradient(45deg, var(--accent) 0 1px, transparent 1px 14px); }
.ma-pat-radial::before { background-image: radial-gradient(circle at 78% 18%, var(--accent) 0%, transparent 55%); opacity:calc(var(--ma-pat-opacity) + 0.05); }
.ma-pat-waves::before { background-image: repeating-radial-gradient(circle at 50% -30%, transparent 0 20px, var(--accent) 20px 21px); }
.ma-pat-mesh::before { background-image: radial-gradient(circle at 12% 18%, var(--accent) 0%, transparent 42%), radial-gradient(circle at 88% 82%, var(--accent) 0%, transparent 42%); opacity:calc(var(--ma-pat-opacity) + 0.03); }
.ma-top { display:flex; justify-content:flex-end; }
.ma-badge {
display:inline-flex; align-items:center; height:22px; padding:0 10px; border-radius:999px;
font-size:9.5px; font-weight:600; letter-spacing:0.07em;
color: var(--accent); background: color-mix(in srgb, var(--accent) var(--ma-badge-mix), transparent);
}
.ma-badge.off { color: var(--ma-off-fg); background: var(--ma-off-bg); }
.ma-title { font-size:22px; font-weight:600; letter-spacing:-0.02em; color:var(--ma-title); margin:18px 0 8px; }
.ma-desc {
font-size:15px; font-weight:400; line-height:1.45; color:var(--ma-desc); margin:0;
display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.ma-footer { margin-top:auto; padding-top:22px; display:flex; align-items:center; justify-content:space-between; gap:12px; }
.ma-source { font-size:12px; font-weight:600; color:var(--accent); background: color-mix(in srgb, var(--accent) var(--ma-pill-mix), transparent); padding:5px 11px; border-radius:999px; }
.ma-lastrun { font-size:12px; color:var(--ma-lastrun); }
.ma-new {
min-height:252px; border-radius:20px; border:1px dashed var(--ma-new-border);
background:transparent; display:flex; flex-direction:column; align-items:center; justify-content:center;
gap:8px; color:var(--ma-new-title); cursor:default; transition: border-color .2s ease, color .2s ease;
}
.ma-new-title { font-size:15px; font-weight:600; color:var(--ma-new-title); }
.ma-new-hint { font-size:12px; color:var(--ma-new-hint); }
`
function Card({ app, onOpen }: { app: MiniApp; onOpen: () => void }) {
const theme = themeFor(app.id)
const pattern = patternFor(app.id)
return (
<button
type="button"
onClick={onOpen}
className={`ma-card ma-pat-${pattern}`}
style={{ '--accent': theme.accent, '--glow': theme.glow } as React.CSSProperties}
>
<div className="ma-top">
<span className={`ma-badge${app.active ? '' : ' off'}`}>
{app.active ? 'ACTIVE' : 'PAUSED'}
</span>
</div>
<div className="ma-title">{app.name}</div>
<div className="ma-desc">{app.description}</div>
<div className="ma-footer">
<span className="ma-source">{app.source}</span>
<span className="ma-lastrun">Last run {app.lastRun}</span>
</div>
</button>
)
}
export function MiniAppsView() {
const [selectedId, setSelectedId] = useState<string | null>(null)
const selected = selectedId ? getMiniApp(selectedId) : undefined
if (selected) {
return (
<div className="flex h-full flex-col">
<div className="flex items-center gap-2 border-b border-border px-3 py-2">
<button
type="button"
onClick={() => setSelectedId(null)}
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
<ArrowLeft className="size-4" />
Apps
</button>
<span className="text-sm font-medium">{selected.name}</span>
</div>
<div className="min-h-0 flex-1">
<MiniAppFrame app={selected} />
</div>
</div>
)
}
return (
<div className="ma-page">
<style>{CARD_CSS}</style>
<div className="ma-inner">
<h1 className="ma-h1">Mini Apps</h1>
<p className="ma-sub">Little apps that live inside Rowboat, powered by your agents and integrations.</p>
<div className="ma-grid">
{MINI_APPS.map((app) => (
<Card key={app.id} app={app} onOpen={() => setSelectedId(app.id)} />
))}
{/* Placeholder for copilot-generated apps (Phase 3). */}
<div className="ma-new">
<Plus className="size-5" />
<div className="ma-new-title">New app</div>
<div className="ma-new-hint">Describe one to the copilot (coming soon)</div>
</div>
</div>
</div>
</div>
)
}

View file

@ -13,6 +13,7 @@ import {
Globe, Globe,
AlertTriangle, AlertTriangle,
Home, Home,
LayoutGrid,
Mic, Mic,
SquarePen, SquarePen,
Plug, Plug,
@ -161,6 +162,7 @@ type SidebarContentPanelProps = {
onOpenMeetings?: () => void onOpenMeetings?: () => void
onOpenCode?: () => void onOpenCode?: () => void
onOpenBgTasks?: () => void onOpenBgTasks?: () => void
onOpenApps?: () => void
onOpenAgent?: (slug: string) => void onOpenAgent?: (slug: string) => void
recentRuns?: { id: string; title?: string; createdAt: string; modifiedAt?: string }[] recentRuns?: { id: string; title?: string; createdAt: string; modifiedAt?: string }[]
onOpenRun?: (runId: string) => void onOpenRun?: (runId: string) => void
@ -171,7 +173,7 @@ type SidebarContentPanelProps = {
onToggleBrowser?: () => void onToggleBrowser?: () => void
onVoiceNoteCreated?: (path: string) => void onVoiceNoteCreated?: (path: string) => void
/** Which primary destination is currently active, for nav highlighting. */ /** Which primary destination is currently active, for nav highlighting. */
activeNav?: 'home' | 'email' | 'meetings' | 'code' | 'knowledge' | 'agents' | 'workspaces' | null activeNav?: 'home' | 'email' | 'meetings' | 'code' | 'knowledge' | 'agents' | 'apps' | 'workspaces' | null
/** Live meeting recording state, so the recording row can show its indicator/stop. */ /** Live meeting recording state, so the recording row can show its indicator/stop. */
meetingRecordingState?: 'idle' | 'connecting' | 'recording' | 'stopping' meetingRecordingState?: 'idle' | 'connecting' | 'recording' | 'stopping'
recordingMeetingSource?: string | null recordingMeetingSource?: string | null
@ -410,6 +412,7 @@ export function SidebarContentPanel({
onOpenMeetings, onOpenMeetings,
onOpenCode, onOpenCode,
onOpenBgTasks, onOpenBgTasks,
onOpenApps,
recentRuns = [], recentRuns = [],
onOpenRun, onOpenRun,
onOpenChatHistory, onOpenChatHistory,
@ -848,6 +851,15 @@ export function SidebarContentPanel({
</div> </div>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
isActive={activeNav === 'apps'}
onClick={onOpenApps}
>
<LayoutGrid className="size-4 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate text-muted-foreground">Mini Apps</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton <SidebarMenuButton
isActive={activeNav === 'workspaces'} isActive={activeNav === 'workspaces'}

View file

@ -0,0 +1,46 @@
// Two simple digest-style sample Mini Apps (Newsletter Digest, Competitor Watch).
// They share the list template in ./simple-list and exist mainly so the gallery
// shows the card design across several apps with different accent themes.
import type { MiniApp } from '../types'
import { buildSimpleListHtml } from './simple-list'
export const newsletterDigestApp: MiniApp = {
id: 'newsletter-digest',
name: 'Newsletter Digest',
description: 'Summarises your subscriptions into a single skimmable morning read.',
source: 'Email',
active: true,
lastRun: '1h ago',
scope: ['gmail'],
data: {
title: 'Newsletter Digest',
subtitle: 'Your subscriptions, summarised — 6 this morning',
items: [
{ id: 'n1', title: 'Stratechery', meta: '7 min', body: 'The platform shift to agents mirrors the mobile transition — distribution, not models, decides the winners.' },
{ id: 'n2', title: 'Lennys Newsletter', meta: '5 min', body: 'How three PMs run discovery without a researcher: weekly user calls, a shared notes doc, and ruthless prioritisation.' },
{ id: 'n3', title: 'The Pragmatic Engineer', meta: '9 min', body: 'Inside a staff-level promo packet: scope, impact, and the "without you it wouldnt have happened" test.' },
],
},
html: buildSimpleListHtml('Newsletter Digest'),
}
export const competitorWatchApp: MiniApp = {
id: 'competitor-watch',
name: 'Competitor Watch',
description: 'Tracks launches, pricing changes, and notable posts from your rivals.',
source: 'Web',
active: false,
lastRun: '4h ago',
scope: ['web'],
data: {
title: 'Competitor Watch',
subtitle: '3 updates since yesterday',
items: [
{ id: 'c1', title: 'Acme launched AI Mode', meta: 'pricing', body: 'New $40/mo tier bundles their assistant; older Pro plan unchanged. Positioning leans on “team workspaces”.' },
{ id: 'c2', title: 'Globex changelog', meta: 'product', body: 'Shipped offline sync and a public API. Docs hint at a desktop app in private beta.' },
{ id: 'c3', title: 'Initech blog', meta: 'content', body: 'A “build vs buy” post aimed squarely at your ICP — worth a counter-piece on local-first privacy.' },
],
},
html: buildSimpleListHtml('Competitor Watch'),
}

View file

@ -0,0 +1,43 @@
// Lightweight Mini App template: a clean single-column list of items.
//
// Used for simple "digest"-style apps (newsletter summaries, competitor updates)
// so the gallery shows the card design across multiple apps. Same dependency-free
// vanilla approach as the Twitter client.
import { buildMiniAppHtml } from '../runtime'
export type ListItem = { id: string; title: string; meta: string; body: string }
const style = `
.app { height:100%; overflow:auto; background:#0a0a0b; color:#e7e9ea; }
.wrap { max-width:640px; margin:0 auto; padding:24px 16px; }
.h { font-size:22px; font-weight:600; letter-spacing:-0.02em; margin:0 0 4px; color:#f5f5f5; }
.sub { font-size:13px; color:#71767b; margin:0 0 20px; }
.item { border:1px solid rgba(255,255,255,0.07); background:#141417; border-radius:14px; padding:16px; margin-bottom:12px; }
.it-top { display:flex; justify-content:space-between; align-items:baseline; gap:12px; }
.it-title { font-size:15px; font-weight:600; }
.it-meta { font-size:12px; color:#71767b; flex:0 0 auto; }
.it-body { font-size:14px; line-height:1.5; color:rgba(255,255,255,0.7); margin:6px 0 0; }
.loading { padding:48px 16px; text-align:center; color:#71767b; }
`
const script = `
var root = document.getElementById('root');
var current = null;
function esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function render(){
if(!current){ root.innerHTML = '<div class="app"><div class="loading">Loading…</div></div>'; return; }
var items = (current.items||[]).map(function(it){
return '<div class="item"><div class="it-top"><div class="it-title">'+esc(it.title)+'</div><div class="it-meta">'+esc(it.meta)+'</div></div>'
+ '<p class="it-body">'+esc(it.body)+'</p></div>';
}).join('');
root.innerHTML = '<div class="app"><div class="wrap"><h1 class="h">'+esc(current.title)+'</h1><p class="sub">'+esc(current.subtitle)+'</p>'+items+'</div></div>';
}
window.rowboat.onData(function(d){ current = d; render(); });
render();
window.rowboat.ready();
`
export function buildSimpleListHtml(title: string): string {
return buildMiniAppHtml({ title, style, script })
}

View file

@ -0,0 +1,236 @@
// Sample Mini App: a Twitter/X client.
//
// An X-style single-column timeline of important posts the agent has curated,
// with simple topic chips to filter what you see. Phase 1 ships static `data`
// and stubbed actions; later the bg-tasks engine produces the feed on a trigger
// and the bridge runs real Composio actions.
//
// Dependency-free vanilla JS + CSS so it renders reliably in the sandboxed
// iframe with no network. window.rowboat is the only channel to the host;
// selected topics persist via rowboat.setState.
import { buildMiniAppHtml } from '../runtime'
import type { MiniApp } from '../types'
const data = {
handle: '@you',
topics: ['AI', 'Startups', 'Dev', 'Design'],
posts: [
{
id: 't1',
author: 'Andrej Karpathy',
handle: '@karpathy',
avatar: '🧠',
time: '2h',
topics: ['AI', 'Dev'],
text: 'The hottest new programming language is English. Spend your time crafting the prompt, not the syntax.',
likes: 1240,
reposts: 312,
},
{
id: 't2',
author: 'Vercel',
handle: '@vercel',
avatar: '▲',
time: '5h',
topics: ['Dev'],
text: 'Shipping is a feature. We just cut cold starts by another 40% on the edge runtime.',
likes: 842,
reposts: 96,
},
{
id: 't3',
author: 'Indie Hackers',
handle: '@IndieHackers',
avatar: '🚀',
time: '1h',
topics: ['Startups'],
text: 'You do not need 1,000 true fans. You need 100 who will tell 10 friends each. Build for the tellers.',
likes: 503,
reposts: 121,
},
{
id: 't4',
author: 'Sarah Dev',
handle: '@sarah_builds',
avatar: '👩‍💻',
time: '32m',
topics: ['AI', 'Startups'],
text: 'Anyone using local-first AI desktop apps day to day? Curious what actually sticks vs. demo-ware.',
likes: 88,
reposts: 7,
},
{
id: 't5',
author: 'Design Notes',
handle: '@designnotes',
avatar: '🎨',
time: '3h',
topics: ['Design'],
text: 'Good defaults beat good settings. Every preference you add is a decision you forced on the user.',
likes: 967,
reposts: 204,
},
{
id: 't6',
author: 'Founder Diary',
handle: '@founderdiary',
avatar: '📈',
time: '6h',
topics: ['Startups', 'Design'],
text: 'Talked to 12 users this week. Shipped 0 features. Best week in a month.',
likes: 1502,
reposts: 388,
},
],
}
const style = `
.app { height:100%; overflow:auto; background:#000; color:#e7e9ea; }
.feed { max-width:600px; margin:0 auto; border-left:1px solid #2f3336; border-right:1px solid #2f3336; min-height:100%; }
.header { position:sticky; top:0; backdrop-filter:blur(12px); background:rgba(0,0,0,.65); border-bottom:1px solid #2f3336; padding:12px 16px; z-index:2; }
.h-title { font-size:20px; font-weight:800; margin:0 0 10px; }
.chips { display:flex; gap:8px; flex-wrap:wrap; }
.chip { border:1px solid #536471; background:transparent; color:#e7e9ea; border-radius:999px; padding:5px 14px; font-size:13px; font-weight:600; cursor:pointer; }
.chip.on { background:#1d9bf0; border-color:#1d9bf0; color:#fff; }
.post { display:flex; gap:12px; padding:12px 16px; border-bottom:1px solid #2f3336; }
.post:hover { background:#080808; }
.avatar { width:40px; height:40px; border-radius:50%; background:#16181c; display:flex; align-items:center; justify-content:center; font-size:18px; flex:0 0 auto; }
.pbody { flex:1; min-width:0; }
.line { display:flex; align-items:center; gap:4px; font-size:15px; }
.name { font-weight:700; }
.handle, .dot, .time { color:#71767b; font-weight:400; }
.text { font-size:15px; line-height:1.4; margin:2px 0 0; white-space:pre-wrap; word-wrap:break-word; }
.tags { margin-top:6px; display:flex; gap:8px; flex-wrap:wrap; }
.tag { font-size:12px; color:#1d9bf0; }
.actions { display:flex; justify-content:space-between; max-width:300px; margin-top:8px; }
.action { display:flex; align-items:center; gap:6px; background:none; border:0; color:#71767b; font-size:13px; cursor:pointer; padding:4px; border-radius:999px; }
.action:hover { color:#1d9bf0; }
.action.like.on { color:#f91880; }
.action.repost.on { color:#00ba7c; }
.empty { padding:48px 16px; text-align:center; color:#71767b; font-size:15px; }
.toast { position:fixed; bottom:20px; left:50%; transform:translateX(-50%); background:#1d9bf0; color:#fff; font-size:14px; font-weight:600; padding:10px 18px; border-radius:999px; opacity:0; transition:opacity .2s; pointer-events:none; }
.toast.show { opacity:1; }
.loading { padding:48px 16px; text-align:center; color:#71767b; font-size:15px; }
`
// Vanilla JS. No backticks (so it embeds cleanly); window.rowboat is the bridge.
const script = `
var root = document.getElementById('root');
var current = null;
var selected = null; // selected topic names
var liked = {}; // id -> bool
var reposted = {}; // id -> bool
var toastTimer = null;
function esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function fmt(n) {
return n >= 1000 ? (n / 1000).toFixed(1).replace('.0', '') + 'K' : String(n);
}
function persist() {
window.rowboat.setState({ topics: selected, liked: liked, reposted: reposted });
}
function postHtml(p) {
var likeOn = liked[p.id] ? ' on' : '';
var repoOn = reposted[p.id] ? ' on' : '';
var likeCt = p.likes + (liked[p.id] ? 1 : 0);
var repoCt = p.reposts + (reposted[p.id] ? 1 : 0);
var tags = (p.topics || []).map(function (t) { return '<span class="tag">#' + esc(t) + '</span>'; }).join('');
return '<div class="post">'
+ '<div class="avatar">' + esc(p.avatar) + '</div>'
+ '<div class="pbody">'
+ '<div class="line"><span class="name">' + esc(p.author) + '</span>'
+ '<span class="handle">' + esc(p.handle) + '</span><span class="dot">·</span><span class="time">' + esc(p.time) + '</span></div>'
+ '<p class="text">' + esc(p.text) + '</p>'
+ (tags ? '<div class="tags">' + tags + '</div>' : '')
+ '<div class="actions">'
+ '<button class="action reply" data-action="reply" data-id="' + p.id + '">💬</button>'
+ '<button class="action repost' + repoOn + '" data-action="repost" data-id="' + p.id + '">🔁 <span class="ct">' + fmt(repoCt) + '</span></button>'
+ '<button class="action like' + likeOn + '" data-action="like" data-id="' + p.id + '">' + (liked[p.id] ? '♥' : '♡') + ' <span class="ct">' + fmt(likeCt) + '</span></button>'
+ '</div></div></div>';
}
function render() {
if (!current) { root.innerHTML = '<div class="app"><div class="feed"><div class="loading">Loading your feed…</div></div></div>'; return; }
var chips = current.topics.map(function (t) {
var on = selected.indexOf(t) >= 0 ? ' on' : '';
return '<button class="chip' + on + '" data-topic="' + esc(t) + '">' + esc(t) + '</button>';
}).join('');
var visible = current.posts.filter(function (p) {
return (p.topics || []).some(function (t) { return selected.indexOf(t) >= 0; });
});
var body = visible.length
? visible.map(postHtml).join('')
: '<div class="empty">No posts. Pick a topic above to see what is happening.</div>';
root.innerHTML = '<div class="app"><div class="feed">'
+ '<div class="header"><h1 class="h-title">Home</h1><div class="chips">' + chips + '</div></div>'
+ body
+ '</div><div class="toast" id="toast"></div></div>';
}
function flash(msg) {
var el = document.getElementById('toast');
if (!el) return;
el.textContent = msg;
el.className = 'toast show';
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () { el.className = 'toast'; }, 2000);
}
root.addEventListener('click', function (e) {
var t = e.target;
var chip = t && t.closest ? t.closest('.chip') : null;
if (chip) {
var topic = chip.getAttribute('data-topic');
var i = selected.indexOf(topic);
if (i >= 0) selected.splice(i, 1); else selected.push(topic);
persist();
render();
return;
}
var btn = t && t.closest ? t.closest('.action') : null;
if (!btn) return;
var action = btn.getAttribute('data-action');
var id = btn.getAttribute('data-id');
if (action === 'reply') { flash('Reply drafted (demo)'); return; }
if (action === 'like') { liked[id] = !liked[id]; }
if (action === 'repost') { reposted[id] = !reposted[id]; }
persist();
render();
window.rowboat.callAction('twitter', action, { id: id }).then(function (res) {
flash((res && res.message) || 'Done');
}).catch(function (err) {
flash('Failed: ' + (err && err.message ? err.message : err));
});
});
window.rowboat.onData(function (d) {
current = d;
if (selected === null) selected = d.topics.slice();
render();
});
window.rowboat.onState(function (st) {
if (!st) return;
if (st.topics) selected = st.topics;
if (st.liked) liked = st.liked;
if (st.reposted) reposted = st.reposted;
if (current) render();
});
render();
window.rowboat.ready();
`
export const twitterClientApp: MiniApp = {
id: 'twitter-client',
name: 'Twitter, curated',
description: 'An X-style feed of the posts that matter, filtered by your topics.',
source: 'Twitter',
active: true,
lastRun: '2m ago',
scope: ['twitter'],
data,
html: buildMiniAppHtml({ title: 'Twitter, curated', style, script }),
}

View file

@ -0,0 +1,14 @@
// Mini Apps registry.
//
// Phase 1: apps are hardcoded here. Later phases replace this with apps loaded
// from ~/.rowboat/apps/<id>/ over IPC.
import type { MiniApp } from './types'
import { twitterClientApp } from './apps/twitter-client'
import { newsletterDigestApp, competitorWatchApp } from './apps/digests'
export const MINI_APPS: MiniApp[] = [twitterClientApp, newsletterDigestApp, competitorWatchApp]
export function getMiniApp(id: string): MiniApp | undefined {
return MINI_APPS.find((app) => app.id === id)
}

View file

@ -0,0 +1,108 @@
// Mini Apps — app HTML scaffolding shared by every app.
//
// `buildMiniAppHtml` wraps an app's markup/JS in a single self-contained HTML
// document and injects the `window.rowboat` bridge shim before the app runs.
//
// Phase 1 is deliberately dependency-free: NO remote CDNs and NO in-browser
// transpile. Apps are plain HTML + CSS + JS so they render reliably inside the
// sandboxed, opaque-origin iframe and work offline. Later phases can layer in a
// locally-bundled React runtime (esbuild-at-save) without changing the bridge.
/**
* The bridge shim, injected as a plain <script> before the app's script. It
* defines `window.rowboat` and speaks the postMessage protocol in ./types.ts.
* This is the only channel the app has to the host.
*/
const BRIDGE_SHIM = /* js */ `
(function () {
var data = null, state = null;
var dataCbs = [], stateCbs = [];
var pending = {}, seq = 0;
function post(msg) { parent.postMessage(msg, '*'); }
window.addEventListener('message', function (e) {
var m = e.data;
if (!m || typeof m !== 'object') return;
if (m.type === 'rowboat:mini-app:data') {
data = m.data;
dataCbs.forEach(function (cb) { try { cb(data); } catch (_) {} });
} else if (m.type === 'rowboat:mini-app:state') {
state = m.state;
stateCbs.forEach(function (cb) { try { cb(state); } catch (_) {} });
} else if (m.type === 'rowboat:mini-app:action-result') {
var p = pending[m.id];
if (p) {
delete pending[m.id];
if (m.ok) p.resolve(m.result); else p.reject(new Error(m.error || 'action failed'));
}
}
});
window.rowboat = {
getData: function () { return data; },
onData: function (cb) {
dataCbs.push(cb);
if (data !== null) { try { cb(data); } catch (_) {} }
return function () { var i = dataCbs.indexOf(cb); if (i >= 0) dataCbs.splice(i, 1); };
},
getState: function () { return state; },
onState: function (cb) {
stateCbs.push(cb);
if (state !== null) { try { cb(state); } catch (_) {} }
return function () { var i = stateCbs.indexOf(cb); if (i >= 0) stateCbs.splice(i, 1); };
},
setState: function (patch) {
state = Object.assign({}, state || {}, patch);
post({ type: 'rowboat:mini-app:setState', patch: patch });
stateCbs.forEach(function (cb) { try { cb(state); } catch (_) {} });
},
callAction: function (scope, action, args) {
var id = 'a' + (++seq);
return new Promise(function (resolve, reject) {
pending[id] = { resolve: resolve, reject: reject };
post({ type: 'rowboat:mini-app:action', id: id, scope: scope, action: action, args: args });
});
},
ready: function () { post({ type: 'rowboat:mini-app:ready' }); },
};
})();
`
/**
* Build a complete self-contained HTML document for a Mini App.
* @param title Document title.
* @param style App CSS (inlined into a <style> tag).
* @param body Initial body markup (often just a mount node).
* @param script App JS. Runs after the bridge shim; `window.rowboat` is ready.
*/
export function buildMiniAppHtml({
title,
style = '',
body = '<div id="root"></div>',
script,
}: {
title: string
style?: string
body?: string
script: string
}): string {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${title}</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
${style}
</style>
</head>
<body>
${body}
<script>${BRIDGE_SHIM}</script>
<script>
${script}
</script>
</body>
</html>`
}

View file

@ -0,0 +1,74 @@
// Mini Apps — shared types and the host <-> iframe bridge protocol.
//
// Phase 1 is UI-only: apps are hardcoded in the renderer (see registry.ts) and
// rendered in a sandboxed iframe (see components/mini-app-frame.tsx). The shapes
// here intentionally mirror the eventual on-disk model (one self-contained folder
// per app under ~/.rowboat/apps/<id>/) so later phases can slot in without a
// rewrite.
/** A single Mini App. */
export type MiniApp = {
/** Stable slug; also the eventual on-disk folder name. The card's accent
* theme and decorative pattern are derived deterministically from this. */
id: string
/** Display name shown on the card and in the open view. */
name: string
/** One-line description for the card (clamped to 2 lines). */
description: string
/** Primary integration shown in the card footer pill (e.g. 'Twitter'). */
source: string
/** Whether the app's agent is currently active (drives the status badge). */
active: boolean
/** Human last-run label for the card footer (e.g. '2m ago'). Static in V1. */
lastRun: string
/**
* Composio integration scope this app is allowed to touch. Drives bridge
* enforcement and the auth prompt in later phases; informational in Phase 1.
*/
scope: string[]
/**
* The app's frontend: a single self-contained HTML document (React + Tailwind
* + Babel via CDN). Rendered via the iframe `srcdoc` attribute.
*/
html: string
/**
* The latest agent "backend" output. Static in Phase 1; produced by the agent
* on a trigger in later phases. Delivered to the iframe via the bridge.
*/
data: unknown
}
// ---------------------------------------------------------------------------
// Bridge protocol (host <-> iframe via postMessage).
//
// The app code inside the iframe talks to a small `window.rowboat` shim (injected
// as part of the app HTML) which speaks these messages. This is both the product
// surface the app codes against and the security boundary.
// ---------------------------------------------------------------------------
/** Messages sent from the iframe (app) up to the host (renderer). */
export type MiniAppOutboundMessage =
/** Handshake: app is mounted and wants its initial data + state. */
| { type: 'rowboat:mini-app:ready' }
/** App requests a scoped Composio action. Host replies with action-result. */
| { type: 'rowboat:mini-app:action'; id: string; scope: string; action: string; args?: unknown }
/** App wants to persist a patch to its per-app state store. */
| { type: 'rowboat:mini-app:setState'; patch: unknown }
/** Messages sent from the host (renderer) down to the iframe (app). */
export type MiniAppInboundMessage =
/** Latest agent data; sent on ready and whenever data refreshes. */
| { type: 'rowboat:mini-app:data'; data: unknown }
/** Current per-app state; sent on ready and after setState. */
| { type: 'rowboat:mini-app:state'; state: unknown }
/** Result of a previously requested action, correlated by id. */
| { type: 'rowboat:mini-app:action-result'; id: string; ok: boolean; result?: unknown; error?: string }
export const MINI_APP_MESSAGE = {
ready: 'rowboat:mini-app:ready',
action: 'rowboat:mini-app:action',
setState: 'rowboat:mini-app:setState',
data: 'rowboat:mini-app:data',
state: 'rowboat:mini-app:state',
actionResult: 'rowboat:mini-app:action-result',
} as const