From 7acbf67cd4e7d53b704e27c91b7094644e475b67 Mon Sep 17 00:00:00 2001 From: Gagan Date: Tue, 30 Jun 2026 00:30:29 +0530 Subject: [PATCH 01/17] 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 && (