From e1fae252bf8dab7fb343189fbe7d4c144cb653e7 Mon Sep 17 00:00:00 2001 From: Prakhar Pandey Date: Tue, 23 Jun 2026 18:11:42 +0530 Subject: [PATCH 01/65] feat: add out-of-credits warning to sidebar plan badge Show a warning state + popover on the sidebar plan badge when the user is out of credits, prompting them to upgrade. Detection comes from two existing signals: the /v1/me billing payload (daily or monthly bucket exhausted, the same data the Settings usage bars use) and the existing 'not enough credits' API-error path. The warning clears on a successful cost-incurring call (LLM finish-step or a successful voice synth) and re-fetches /me once, only when the warning was actually showing -- no polling. Popover auto-opens on first detection and can be dismissed while the indicator persists. --- apps/x/apps/renderer/src/App.tsx | 3 + .../src/components/sidebar-content.tsx | 156 +++++++++++++++--- .../renderer/src/components/ui/popover.tsx | 15 +- apps/x/apps/renderer/src/hooks/useVoiceTTS.ts | 10 +- apps/x/apps/renderer/src/lib/billing-error.ts | 3 + apps/x/apps/renderer/src/lib/credit-status.ts | 26 +++ 6 files changed, 186 insertions(+), 27 deletions(-) create mode 100644 apps/x/apps/renderer/src/lib/credit-status.ts diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 9d333581..4e4ca730 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -76,6 +76,7 @@ import { Button } from "@/components/ui/button" import { Toaster } from "@/components/ui/sonner" import { BillingErrorDialog } from "@/components/billing-error-dialog" import { matchBillingError, type BillingErrorMatch } from "@/lib/billing-error" +import { dispatchCreditExhausted, dispatchCreditReplenished } from "@/lib/credit-status" import { ensureMarkdownExtension, normalizeWikiPath, splitWikiFragment, stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-links' import { splitFrontmatter, joinFrontmatter } from '@/lib/frontmatter' import { extractConferenceLink } from '@/lib/calendar-event' @@ -923,6 +924,7 @@ function App() { lastHandledBillingErrorIdRef.current = item.id setBillingErrorMatch(match) setBillingErrorOpen(true) + if (match.kind === 'out_of_credits') dispatchCreditExhausted() } return } @@ -2370,6 +2372,7 @@ function App() { const nextUsage = normalizeUsage(llmEvent.usage) if (nextUsage) { setModelUsage(nextUsage) + dispatchCreditReplenished() } } } diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index bd3d7888..b06f5bc7 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -22,6 +22,8 @@ import { Settings, Square, Video, + CircleAlert, + X, } from "lucide-react" import { AlertDialog, @@ -51,6 +53,7 @@ import { Popover, PopoverContent, PopoverTrigger, + PopoverArrow, } from "@/components/ui/popover" import { Tooltip, @@ -58,6 +61,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" +import { isOutOfCredits, CREDIT_EXHAUSTED_EVENT, CREDIT_REPLENISHED_EVENT } from "@/lib/credit-status" import { SettingsDialog } from "@/components/settings-dialog" import { extractConferenceLink } from "@/lib/calendar-event" import { useBilling } from "@/hooks/useBilling" @@ -430,9 +434,13 @@ export function SidebarContentPanel({ const [openConnectionsAfterClose, setOpenConnectionsAfterClose] = useState(false) const connectorsButtonRef = useRef(null) const [isRowboatConnected, setIsRowboatConnected] = useState(false) + const [creditPopoverOpen, setCreditPopoverOpen] = useState(false) + const [outOfCredits, setOutOfCredits] = useState(false) + const outOfCreditsRef = useRef(false) + const creditPopoverAutoShownRef = useRef(false) const [loggingIn, setLoggingIn] = useState(false) const [appUrl, setAppUrl] = useState(null) - const { billing } = useBilling(isRowboatConnected) + const { billing, refresh: refreshBilling } = useBilling(isRowboatConnected) const currentBillingPlan = billing ? getBillingPlanData(billing.catalog, billing.subscriptionPlanId) : null // Nav previews: unread important emails + next upcoming meetings (top 2 each). @@ -654,6 +662,50 @@ export function SidebarContentPanel({ } }, []) + // Re-anchor the warning whenever billing (re)loads — billing is authoritative. + useEffect(() => { + if (billing) { + const next = isOutOfCredits(billing) + outOfCreditsRef.current = next + setOutOfCredits(next) + } + }, [billing]) + + // Live signals: a usage API error flips it on; a successful cost-incurring + // call flips it off and triggers a single billing refresh to reconcile. + useEffect(() => { + const onExhausted = () => { + outOfCreditsRef.current = true + setOutOfCredits(true) + } + const onReplenished = () => { + const wasOut = outOfCreditsRef.current + outOfCreditsRef.current = false + setOutOfCredits(false) + if (wasOut) void refreshBilling() + } + window.addEventListener(CREDIT_EXHAUSTED_EVENT, onExhausted) + window.addEventListener(CREDIT_REPLENISHED_EVENT, onReplenished) + return () => { + window.removeEventListener(CREDIT_EXHAUSTED_EVENT, onExhausted) + window.removeEventListener(CREDIT_REPLENISHED_EVENT, onReplenished) + } + }, [refreshBilling]) + + // Auto-open the popover the first time we go out of credits; reset when + // credits return so it can auto-open again on a future episode. + useEffect(() => { + if (outOfCredits) { + if (!creditPopoverAutoShownRef.current) { + creditPopoverAutoShownRef.current = true + setCreditPopoverOpen(true) + } + } else { + creditPopoverAutoShownRef.current = false + setCreditPopoverOpen(false) + } + }, [outOfCredits]) + // Single preview shown as a sublabel on the Email / Meetings nav buttons. const previewEmail = emailThreads[0] const previewMeeting = meetings[0] @@ -913,31 +965,89 @@ export function SidebarContentPanel({ {/* Billing / upgrade CTA or Log in CTA */} - {isRowboatConnected && billing ? ( -
-
-
- - {currentBillingPlan?.displayName ?? (billing.subscriptionPlanId ? 'Unknown' : 'No plan')} - - {billing.subscriptionStatus === 'trialing' && billing.trialExpiresAt && (() => { - const days = Math.max(0, Math.ceil((new Date(billing.trialExpiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24))) - return ( -

- {days === 0 ? 'Trial expires today' : days === 1 ? '1 day left' : `${days} days left`} + {isRowboatConnected && billing ? (() => { + const upgradeLabel = !billing.subscriptionPlanId || currentBillingPlan?.category === 'free' || currentBillingPlan?.category === 'starter' ? 'Upgrade' : 'Manage' + if (outOfCredits) { + return ( +

+ +
+ + + + +
+ + +
+
+ + + +

You've run out of credits

+
+ +
+

+ Upgrade your plan to continue using all features.

- ) - })()} + +
+
+
+ ) + } + return ( +
+
+
+ + {currentBillingPlan?.displayName ?? (billing.subscriptionPlanId ? 'Unknown' : 'No plan')} + + {billing.subscriptionStatus === 'trialing' && billing.trialExpiresAt && (() => { + const days = Math.max(0, Math.ceil((new Date(billing.trialExpiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24))) + return ( +

+ {days === 0 ? 'Trial expires today' : days === 1 ? '1 day left' : `${days} days left`} +

+ ) + })()} +
+
-
-
- ) : null} + ) + })() : null} {/* Sign in CTA */} {!isRowboatConnected && (
diff --git a/apps/x/apps/renderer/src/components/ui/popover.tsx b/apps/x/apps/renderer/src/components/ui/popover.tsx index 27f1af3c..5823782d 100644 --- a/apps/x/apps/renderer/src/components/ui/popover.tsx +++ b/apps/x/apps/renderer/src/components/ui/popover.tsx @@ -43,4 +43,17 @@ function PopoverAnchor({ return } -export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } +function PopoverArrow({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverArrow } diff --git a/apps/x/apps/renderer/src/hooks/useVoiceTTS.ts b/apps/x/apps/renderer/src/hooks/useVoiceTTS.ts index c0737bf8..6b805e64 100644 --- a/apps/x/apps/renderer/src/hooks/useVoiceTTS.ts +++ b/apps/x/apps/renderer/src/hooks/useVoiceTTS.ts @@ -1,4 +1,5 @@ import { useCallback, useRef, useState } from 'react'; +import { dispatchCreditReplenished } from '@/lib/credit-status'; export type TTSState = 'idle' | 'synthesizing' | 'speaking'; @@ -8,9 +9,12 @@ interface SynthesizedAudio { function synthesize(text: string): Promise { return window.ipc.invoke('voice:synthesize', { text }).then( - (result: { audioBase64: string; mimeType: string }) => ({ - dataUrl: `data:${result.mimeType};base64,${result.audioBase64}`, - }) + (result: { audioBase64: string; mimeType: string }) => { + // A successful Rowboat voice synth is a cost-incurring call that + // returned OK, so it proves credits are available again. + dispatchCreditReplenished(); + return { dataUrl: `data:${result.mimeType};base64,${result.audioBase64}` }; + } ); } diff --git a/apps/x/apps/renderer/src/lib/billing-error.ts b/apps/x/apps/renderer/src/lib/billing-error.ts index 2e6736ed..fc0cc896 100644 --- a/apps/x/apps/renderer/src/lib/billing-error.ts +++ b/apps/x/apps/renderer/src/lib/billing-error.ts @@ -1,17 +1,20 @@ export const BILLING_ERROR_PATTERNS = [ { + kind: 'subscription_required', pattern: /upgrade required/i, title: 'A subscription is required', subtitle: 'Get started with a plan to access AI features in Rowboat.', cta: 'Subscribe', }, { + kind: 'out_of_credits', pattern: /not enough credits/i, title: "You've run out of credits", subtitle: 'Upgrade your plan for more usage. Daily usage resets at 00:00 UTC.', cta: 'Upgrade plan', }, { + kind: 'subscription_inactive', pattern: /subscription not active/i, title: 'Your subscription is inactive', subtitle: 'Reactivate your subscription to continue using AI features.', diff --git a/apps/x/apps/renderer/src/lib/credit-status.ts b/apps/x/apps/renderer/src/lib/credit-status.ts new file mode 100644 index 00000000..2cdc2284 --- /dev/null +++ b/apps/x/apps/renderer/src/lib/credit-status.ts @@ -0,0 +1,26 @@ +import type { BillingInfo } from '@x/shared/dist/billing.js' + +/** + * A user is "out of credits" when EITHER the daily or the monthly bucket is + * exhausted. Either exhaustion is what triggers the backend's "not enough + * credits" API error, and this mirrors the two usage bars shown in Settings + * (Plan usage = monthly, Daily use = daily). `availableCredits` already comes + * down in the /v1/me payload, so no extra API work is needed. + */ +export function isOutOfCredits(billing: BillingInfo): boolean { + return billing.daily.availableCredits <= 0 || billing.monthly.availableCredits <= 0 +} + +/** Fired when we learn the user is out of credits (billing data or a usage API error). */ +export const CREDIT_EXHAUSTED_EVENT = 'credit-status:exhausted' + +/** Fired when a successful cost-incurring call (LLM / voice) proves credits are available again. */ +export const CREDIT_REPLENISHED_EVENT = 'credit-status:replenished' + +export function dispatchCreditExhausted(): void { + window.dispatchEvent(new Event(CREDIT_EXHAUSTED_EVENT)) +} + +export function dispatchCreditReplenished(): void { + window.dispatchEvent(new Event(CREDIT_REPLENISHED_EVENT)) +} From 7acbf67cd4e7d53b704e27c91b7094644e475b67 Mon Sep 17 00:00:00 2001 From: Gagan Date: Tue, 30 Jun 2026 00:30:29 +0530 Subject: [PATCH 02/65] 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 --- apps/x/MINI_APPS_PLAN.md | 169 +++++++++++++ apps/x/apps/renderer/src/App.tsx | 221 ++++++++++------ .../src/components/mini-app-frame.tsx | 96 +++++++ .../src/components/mini-apps-view.tsx | 202 +++++++++++++++ .../src/components/sidebar-content.tsx | 14 +- .../renderer/src/mini-apps/apps/digests.ts | 46 ++++ .../src/mini-apps/apps/simple-list.ts | 43 ++++ .../src/mini-apps/apps/twitter-client.ts | 236 ++++++++++++++++++ .../x/apps/renderer/src/mini-apps/registry.ts | 14 ++ apps/x/apps/renderer/src/mini-apps/runtime.ts | 108 ++++++++ apps/x/apps/renderer/src/mini-apps/types.ts | 74 ++++++ 11 files changed, 1152 insertions(+), 71 deletions(-) create mode 100644 apps/x/MINI_APPS_PLAN.md create mode 100644 apps/x/apps/renderer/src/components/mini-app-frame.tsx create mode 100644 apps/x/apps/renderer/src/components/mini-apps-view.tsx create mode 100644 apps/x/apps/renderer/src/mini-apps/apps/digests.ts create mode 100644 apps/x/apps/renderer/src/mini-apps/apps/simple-list.ts create mode 100644 apps/x/apps/renderer/src/mini-apps/apps/twitter-client.ts create mode 100644 apps/x/apps/renderer/src/mini-apps/registry.ts create mode 100644 apps/x/apps/renderer/src/mini-apps/runtime.ts create mode 100644 apps/x/apps/renderer/src/mini-apps/types.ts diff --git a/apps/x/MINI_APPS_PLAN.md b/apps/x/MINI_APPS_PLAN.md new file mode 100644 index 00000000..353883be --- /dev/null +++ b/apps/x/MINI_APPS_PLAN.md @@ -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 // 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 ? (`. + +### 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//` 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. diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index bfe71c85..e55bfb03 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -27,6 +27,7 @@ import { SidebarContentPanel } from '@/components/sidebar-content'; import { SuggestedTopicsView } from '@/components/suggested-topics-view'; import { LiveNotesView } from '@/components/live-notes-view'; import { BgTasksView } from '@/components/bg-tasks-view'; +import { MiniAppsView } from '@/components/mini-apps-view'; import { EmailView } from '@/components/email-view'; import { WorkspaceView } from '@/components/workspace-view'; 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 LIVE_NOTES_TAB_PATH = '__rowboat_live_notes__' const BG_TASKS_TAB_PATH = '__rowboat_bg_tasks__' +const APPS_TAB_PATH = '__rowboat_mini_apps__' const EMAIL_TAB_PATH = '__rowboat_email__' const WORKSPACE_TAB_PATH = '__rowboat_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 isLiveNotesTabPath = (path: string) => path === LIVE_NOTES_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 isWorkspaceTabPath = (path: string) => path === WORKSPACE_TAB_PATH const isKnowledgeViewTabPath = (path: string) => path === KNOWLEDGE_VIEW_TAB_PATH @@ -634,6 +637,7 @@ type ViewState = | { type: 'home' } | { type: 'code' } | { type: 'bg-tasks' } + | { type: 'apps' } function viewStatesEqual(a: ViewState, b: ViewState): boolean { if (a.type !== b.type) return false @@ -712,6 +716,8 @@ function parseDeepLink(input: string): ViewState | null { return { type: 'code' } case 'bg-tasks': return { type: 'bg-tasks' } + case 'apps': + return { type: 'apps' } default: return null } @@ -823,6 +829,7 @@ function App() { const [isMeetingsOpen, setIsMeetingsOpen] = useState(false) const [isLiveNotesOpen, setIsLiveNotesOpen] = useState(false) const [isBgTasksOpen, setIsBgTasksOpen] = useState(false) + const [isAppsOpen, setIsAppsOpen] = useState(false) const [isEmailOpen, setIsEmailOpen] = useState(false) const [isWorkspaceOpen, setIsWorkspaceOpen] = useState(false) const [workspaceInitialPath, setWorkspaceInitialPath] = useState(null) @@ -1251,6 +1258,7 @@ function App() { if (isMeetingsTabPath(tab.path)) return 'Meetings' if (isLiveNotesTabPath(tab.path)) return 'Live notes' if (isBgTasksTabPath(tab.path)) return 'Background tasks' + if (isAppsTabPath(tab.path)) return 'Mini Apps' if (isEmailTabPath(tab.path)) return 'Email' if (isWorkspaceTabPath(tab.path)) return 'Workspace' if (isKnowledgeViewTabPath(tab.path)) return 'Brain' @@ -3214,7 +3222,7 @@ function App() { setActiveFileTabId(existingTab.id) setIsGraphOpen(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) return } @@ -3223,7 +3231,7 @@ function App() { setActiveFileTabId(id) setIsGraphOpen(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) }, [fileTabs, dismissBrowserOverlay]) @@ -3242,14 +3250,14 @@ function App() { setSelectedPath(null) setIsGraphOpen(true) 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 } if (isSuggestedTopicsTabPath(tab.path)) { setSelectedPath(null) setIsGraphOpen(false) 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 } if (isLiveNotesTabPath(tab.path)) { @@ -3262,7 +3270,7 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setIsLiveNotesOpen(true) return } @@ -3276,10 +3284,25 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setIsBgTasksOpen(true) 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)) { setSelectedPath(null) setIsGraphOpen(false) @@ -3291,7 +3314,7 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) return } if (isEmailTabPath(tab.path)) { @@ -3304,7 +3327,7 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setIsEmailOpen(true) return } @@ -3318,7 +3341,7 @@ function App() { setIsEmailOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setIsWorkspaceOpen(true) return } @@ -3332,7 +3355,7 @@ function App() { setIsEmailOpen(false) setIsWorkspaceOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setIsKnowledgeViewOpen(true) return } @@ -3346,7 +3369,7 @@ function App() { setIsEmailOpen(false) setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) - setIsChatHistoryOpen(true); setIsHomeOpen(false) + setIsChatHistoryOpen(true); setIsHomeOpen(false); setIsAppsOpen(false) return } if (isHomeTabPath(tab.path)) { @@ -3354,7 +3377,7 @@ function App() { setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false) - setIsHomeOpen(true) + setIsHomeOpen(true); setIsAppsOpen(false) return } if (isCodeTabPath(tab.path)) { @@ -3362,18 +3385,18 @@ function App() { setSelectedPath(null) setIsGraphOpen(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 } setIsGraphOpen(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) }, [fileTabs, isRightPaneMaximized, dismissBrowserOverlay]) const closeFileTab = useCallback((tabId: string) => { 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) initialContentByPathRef.current.delete(closingTab.path) untitledRenameReadyPathsRef.current.delete(closingTab.path) @@ -3396,7 +3419,7 @@ function App() { setSelectedPath(null) setIsGraphOpen(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 [] } const idx = prev.findIndex(t => t.id === tabId) @@ -3410,12 +3433,12 @@ function App() { setSelectedPath(null) setIsGraphOpen(true) 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)) { setSelectedPath(null) setIsGraphOpen(false) 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)) { setSelectedPath(null) setIsGraphOpen(false) @@ -3427,7 +3450,7 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) } else if (isLiveNotesTabPath(newActiveTab.path)) { setSelectedPath(null) setIsGraphOpen(false) @@ -3438,7 +3461,7 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setIsLiveNotesOpen(true) } else if (isBgTasksTabPath(newActiveTab.path)) { setSelectedPath(null) @@ -3451,7 +3474,20 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(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)) { setSelectedPath(null) setIsGraphOpen(false) @@ -3462,7 +3498,7 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setIsEmailOpen(true) } else if (isWorkspaceTabPath(newActiveTab.path)) { setSelectedPath(null) @@ -3474,7 +3510,7 @@ function App() { setIsEmailOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setIsWorkspaceOpen(true) } else if (isKnowledgeViewTabPath(newActiveTab.path)) { setSelectedPath(null) @@ -3486,7 +3522,7 @@ function App() { setIsEmailOpen(false) setIsWorkspaceOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setIsKnowledgeViewOpen(true) } else if (isChatHistoryTabPath(newActiveTab.path)) { setSelectedPath(null) @@ -3498,17 +3534,17 @@ function App() { setIsEmailOpen(false) setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) - setIsChatHistoryOpen(true); setIsHomeOpen(false) + setIsChatHistoryOpen(true); setIsHomeOpen(false); setIsAppsOpen(false) } else if (isHomeTabPath(newActiveTab.path)) { setSelectedPath(null) setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false) - setIsHomeOpen(true) + setIsHomeOpen(true); setIsAppsOpen(false) } else { setIsGraphOpen(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) } } @@ -3530,7 +3566,7 @@ function App() { dismissBrowserOverlay() handleNewChat() // 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({ path: selectedPath, graph: isGraphOpen, @@ -3547,8 +3583,8 @@ function App() { setSelectedPath(null) setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) - setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) - }, [dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, isHomeOpen]) + 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, isAppsOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, isHomeOpen]) // Sidebar variant: reset the chat in place without leaving file/graph context. const handleNewChatTabInSidebar = useCallback(() => { @@ -3680,7 +3716,7 @@ function App() { const handleOpenFullScreenChat = useCallback(() => { // 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({ path: selectedPath, graph: isGraphOpen, @@ -3696,8 +3732,8 @@ function App() { setSelectedPath(null) setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) - setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false) - }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, dismissBrowserOverlay]) + setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false) + }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isAppsOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, dismissBrowserOverlay]) const handleCloseFullScreenChat = useCallback((): boolean => { let restored = false @@ -3706,11 +3742,11 @@ function App() { if (expandedFrom.graph) { setIsGraphOpen(true) 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) { setIsGraphOpen(false) 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) { setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) @@ -3742,7 +3778,7 @@ function App() { } else if (expandedFrom.path) { setIsGraphOpen(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) } else { // expandedFrom was captured from a view this restorer doesn't track @@ -3768,10 +3804,11 @@ function App() { if (isHomeOpen) return { type: 'home' } if (isCodeOpen) return { type: 'code' } if (isBgTasksOpen) return { type: 'bg-tasks' } + if (isAppsOpen) return { type: 'apps' } if (selectedPath) return { type: 'file', path: selectedPath } if (isGraphOpen) return { type: 'graph' } 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 last = stack[stack.length - 1] @@ -3861,6 +3898,17 @@ function App() { setActiveFileTabId(id) }, [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 existing = fileTabs.find((tab) => isEmailTabPath(tab.path)) if (existing) { @@ -3938,7 +3986,7 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setSelectedBackgroundTask(null) setExpandedFrom(null) setIsRightPaneMaximized(false) @@ -3955,7 +4003,7 @@ function App() { setIsGraphOpen(false) setIsBrowserOpen(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) setExpandedFrom(null) setIsRightPaneMaximized(false) @@ -3963,6 +4011,19 @@ function App() { 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(() => { setSelectedPath(null) setIsGraphOpen(false) @@ -3975,7 +4036,7 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setSelectedBackgroundTask(null) setExpandedFrom(null) setIsRightPaneMaximized(false) @@ -3987,7 +4048,7 @@ function App() { setIsGraphOpen(false) setIsBrowserOpen(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) setExpandedFrom(null) setIsRightPaneMaximized(false) @@ -4003,7 +4064,7 @@ function App() { // visible in the middle pane. setIsBrowserOpen(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) // Preserve split vs knowledge-max mode when navigating knowledge files. // Only exit chat-only maximize, because that would hide the selected file. @@ -4018,7 +4079,7 @@ function App() { setSelectedPath(null) setIsBrowserOpen(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) setIsGraphOpen(true) ensureGraphFileTab() @@ -4031,7 +4092,7 @@ function App() { setIsGraphOpen(false) setIsBrowserOpen(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) setIsRightPaneMaximized(false) setSelectedBackgroundTask(view.name) @@ -4044,7 +4105,7 @@ function App() { setIsRightPaneMaximized(false) setSelectedBackgroundTask(null) 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() return case 'meetings': @@ -4062,7 +4123,7 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) ensureMeetingsFileTab() return case 'live-notes': @@ -4079,7 +4140,7 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setIsLiveNotesOpen(true) ensureLiveNotesFileTab() return @@ -4098,7 +4159,7 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) // 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. if (view.threadId) { @@ -4122,7 +4183,7 @@ function App() { setIsWorkspaceOpen(true) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) setWorkspaceInitialPath(view.path ?? null) ensureWorkspaceFileTab() return @@ -4143,7 +4204,7 @@ function App() { setKnowledgeViewMode(view.mode ?? (view.folderPath ? 'files' : 'graph')) setKnowledgeViewFolderPath(view.folderPath ?? null) setIsChatHistoryOpen(false) - setIsHomeOpen(false) + setIsHomeOpen(false); setIsAppsOpen(false) ensureKnowledgeViewFileTab() return case 'chat-history': @@ -4160,7 +4221,7 @@ function App() { setIsEmailOpen(false) setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) - setIsChatHistoryOpen(true); setIsHomeOpen(false) + setIsChatHistoryOpen(true); setIsHomeOpen(false); setIsAppsOpen(false) ensureChatHistoryFileTab() return case 'home': @@ -4178,7 +4239,7 @@ function App() { setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(false) setIsChatHistoryOpen(false) - setIsHomeOpen(true) + setIsHomeOpen(true); setIsAppsOpen(false) ensureHomeFileTab() return case 'code': @@ -4189,7 +4250,7 @@ function App() { setIsRightPaneMaximized(false) setSelectedBackgroundTask(null) 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() return case 'bg-tasks': @@ -4200,10 +4261,22 @@ function App() { setIsRightPaneMaximized(false) setSelectedBackgroundTask(null) 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) ensureBgTasksFileTab() 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': setSelectedPath(null) setIsGraphOpen(false) @@ -4212,7 +4285,7 @@ function App() { setIsRightPaneMaximized(false) setSelectedBackgroundTask(null) 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) { const targetRunId = view.runId // Bind the loaded run to a chat tab so its title (derived from @@ -4232,7 +4305,7 @@ function App() { } 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 current = currentViewState @@ -4566,7 +4639,7 @@ function App() { }, []) // 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(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'l') { @@ -4651,11 +4724,11 @@ function App() { const handleTabKeyDown = (e: KeyboardEvent) => { const mod = e.metaKey || e.ctrlKey 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 ? (isRightPaneMaximized ? 'right' : activeShortcutPane) : '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 ? GRAPH_TAB_PATH : isSuggestedTopicsOpen @@ -4666,6 +4739,8 @@ function App() { ? LIVE_NOTES_TAB_PATH : isBgTasksOpen ? BG_TASKS_TAB_PATH + : isAppsOpen + ? APPS_TAB_PATH : isEmailOpen ? EMAIL_TAB_PATH : isWorkspaceOpen @@ -4749,7 +4824,7 @@ function App() { } document.addEventListener('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') => { 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) setIsRightPaneMaximized(false) } @@ -4913,21 +4988,21 @@ function App() { }, openGraph: () => { // 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) setIsRightPaneMaximized(false) } void navigateToView({ type: 'graph' }) }, 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) setIsRightPaneMaximized(false) } void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH }) }, 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) setIsRightPaneMaximized(false) } @@ -5597,7 +5672,7 @@ function App() { const selectedTask = selectedBackgroundTask ? backgroundTasks.find(t => t.name === selectedBackgroundTask) : 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 shouldCollapseLeftPane = isRightPaneOnlyMode const nonChatPaneStyle = React.useMemo(() => { @@ -5645,7 +5720,7 @@ function App() { return ( { - 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 }) } }}> @@ -5668,6 +5743,7 @@ function App() { : isCodeOpen ? 'code' : (isKnowledgeViewOpen || isGraphOpen || (selectedPath != null && selectedPath.startsWith('knowledge/'))) ? 'knowledge' : isBgTasksOpen ? 'agents' + : isAppsOpen ? 'apps' : isWorkspaceOpen ? 'workspaces' : null } @@ -5675,6 +5751,7 @@ function App() { onOpenCode={openCodeView} onOpenBgTasks={() => { setBgTaskInitialSlug(null); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }} onOpenAgent={(slug) => { setBgTaskInitialSlug(slug); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }} + onOpenApps={openAppsView} recentRuns={runs} onOpenRun={(rid) => void navigateToView({ type: 'chat', runId: rid })} onOpenChatHistory={() => void navigateToView({ type: 'chat-history' })} @@ -5707,7 +5784,7 @@ function App() { canNavigateForward={canNavigateForward} 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 ? ( t.id} onSwitchTab={switchFileTab} 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 ? ( Version history )} - {!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 && (
+ ) : isAppsOpen ? ( +
+ +
) : isEmailOpen ? (
diff --git a/apps/x/apps/renderer/src/components/mini-app-frame.tsx b/apps/x/apps/renderer/src/components/mini-app-frame.tsx new file mode 100644 index 00000000..0e988ada --- /dev/null +++ b/apps/x/apps/renderer/src/components/mini-app-frame.tsx @@ -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(null) + // In-memory per-app state for Phase 1 (resets when the frame unmounts). + const stateRef = useRef>({}) + + 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) } + postToFrame({ type: MINI_APP_MESSAGE.state, state: stateRef.current }) + break + } + } + } + + window.addEventListener('message', handleMessage) + return () => window.removeEventListener('message', handleMessage) + }, [app]) + + return ( +