mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
feat(mini-apps): add Mini Apps section with sandboxed app runtime
Phase 1 of Mini Apps: a new sidebar section that renders user apps as premium tiles and opens each in a sandboxed iframe. - New first-class 'apps' view wired through App.tsx (mirrors bg-tasks) - 'Mini Apps' sidebar entry - MiniAppFrame: sandboxed iframe host + window.rowboat bridge (getData/onData, getState/setState, scoped callAction; actions stubbed) - Self-contained vanilla apps (no CDN) so they render offline in the frame - Sample apps: Twitter (X-style feed + topic filters), Newsletter Digest, Competitor Watch - Premium card gallery: accent themes, decorative patterns, ambient glow, light/dark support
This commit is contained in:
parent
753e3448f0
commit
7acbf67cd4
11 changed files with 1152 additions and 71 deletions
169
apps/x/MINI_APPS_PLAN.md
Normal file
169
apps/x/MINI_APPS_PLAN.md
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
# Mini Apps — Implementation Plan
|
||||||
|
|
||||||
|
> Status: **Phase 1 (UI + rendering, hand-coded apps)** in progress.
|
||||||
|
> Branch: `feature/mini-apps`. Working dir: `apps/x`.
|
||||||
|
|
||||||
|
## 1. What we're building (agreed design)
|
||||||
|
|
||||||
|
A **Mini App** is a user-created, personal app that lives inside Rowboat under a
|
||||||
|
**"Mini Apps"** sidebar tab, shown as cards; click a card to open and use it like
|
||||||
|
a standalone app.
|
||||||
|
|
||||||
|
The settled mental model:
|
||||||
|
|
||||||
|
> A Mini App = **custom UI code** (a single self-contained HTML file: React +
|
||||||
|
> Tailwind, no build step, runs in a sandboxed iframe) + an **agent "backend"**
|
||||||
|
> that produces fresh **data** on a trigger + an optional **state store** + a
|
||||||
|
> **scoped bridge** that lets the UI call real **Composio** actions.
|
||||||
|
|
||||||
|
Key decisions (the "why" behind the architecture):
|
||||||
|
|
||||||
|
- **Opens to a finished, populated screen.** The agent runs *before* open (on a
|
||||||
|
trigger); the UI just renders the latest result. Maps onto the existing
|
||||||
|
background-agents engine.
|
||||||
|
- **Frontend/backend split.** Code = frontend, written once. Agent = backend,
|
||||||
|
produces fresh `data.json` each run. The UI is *not* regenerated per run.
|
||||||
|
Cheaper, faster, and the UI can't break on refresh.
|
||||||
|
- **A data contract** ties the three pieces together: at creation the builder
|
||||||
|
produces a matched triple — UI code + data schema + agent instructions.
|
||||||
|
- **Fully custom UI per app** (generated code), not a fixed block kit.
|
||||||
|
- **Real interactivity** via a **scoped bridge** to Composio (each app declares
|
||||||
|
the integrations it may touch; scope drives both enforcement and the auth
|
||||||
|
prompt).
|
||||||
|
- **Built by whatever engine is selected in the chat box** — Code Mode
|
||||||
|
(Claude Code / Codex) if the user has it, else the Copilot.
|
||||||
|
- **Living apps:** refined by chatting; breakage is first-class (app surfaces
|
||||||
|
errors → "fix with copilot").
|
||||||
|
- **State is an optional provided primitive** (per-app persistent store the agent
|
||||||
|
and UI can read/write). Stateless apps ignore it.
|
||||||
|
- **Personal/local only** for now, but each app is a **self-contained folder**
|
||||||
|
so it can become portable later.
|
||||||
|
|
||||||
|
### Framework for the apps
|
||||||
|
|
||||||
|
Single self-contained `index.html` rendered in a sandboxed iframe.
|
||||||
|
|
||||||
|
> **Learning (Phase 1):** remote CDNs do NOT work inside the sandboxed,
|
||||||
|
> opaque-origin `srcdoc` iframe (React/Tailwind/Babel from unpkg/cdn.tailwindcss
|
||||||
|
> failed → blank screen). Apps must be **fully self-contained, no network**.
|
||||||
|
> Phase 1 therefore ships **vanilla HTML + CSS + JS** (no build, no transpile).
|
||||||
|
> The React+Tailwind LLM-friendly target still stands for generation — but via a
|
||||||
|
> **locally-bundled** runtime (esbuild-at-save, libs injected into the HTML),
|
||||||
|
> never remote CDNs. The `window.rowboat` bridge is unchanged either way.
|
||||||
|
|
||||||
|
The UI talks to the host through a `window.rowboat` **bridge** (the product
|
||||||
|
surface *and* the security boundary):
|
||||||
|
|
||||||
|
```
|
||||||
|
rowboat.getData(): Promise<Data> // latest agent output
|
||||||
|
rowboat.onData(cb): void // re-render on refresh
|
||||||
|
rowboat.getState() / setState(patch) // per-app persistent store
|
||||||
|
rowboat.callAction(scope, action, args) // scoped Composio call
|
||||||
|
rowboat.ready() // handshake: "send me data"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Phased roadmap
|
||||||
|
|
||||||
|
1. **Surface + one real app (UI-first — THIS PHASE).** "Mini Apps" sidebar tab,
|
||||||
|
card grid, click to open. One **hardcoded** app (Twitter client) rendered in a
|
||||||
|
sandboxed iframe from a self-contained HTML file, reading static data through a
|
||||||
|
minimal bridge. Proves the *experience* and the *rendering path*. No agent,
|
||||||
|
no storage, no real Composio yet.
|
||||||
|
2. **The scoped bridge.** Real interactive buttons → real Composio actions,
|
||||||
|
enforced against a per-app manifest scope; auth prompt on demand.
|
||||||
|
3. **Generation.** Copilot/Code-Mode generates the UI+schema+agent triple from a
|
||||||
|
chat description. Rides the existing chat mode selector + `code-with-agents`.
|
||||||
|
4. **Living apps + breakage recovery.** Conversational edits; error surfacing.
|
||||||
|
5. **Scale & polish.** Context-overload windowing (e.g. 100 tweets); app
|
||||||
|
notifications (reuse `notify-user`).
|
||||||
|
6. **(V2)** Drag an app into the main workspace (macOS-style).
|
||||||
|
|
||||||
|
## 3. Phase 1 — detailed implementation
|
||||||
|
|
||||||
|
UI-first. Everything hand-coded; no `~/.rowboat` storage, no IPC, no agent yet.
|
||||||
|
We *do* mirror the eventual shapes so later phases slot in.
|
||||||
|
|
||||||
|
### 3.1 New files (renderer)
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `apps/renderer/src/mini-apps/types.ts` | `MiniApp` type (id, name, description, icon, accent, scope, html, data). |
|
||||||
|
| `apps/renderer/src/mini-apps/registry.ts` | Hardcoded list of `MiniApp`s for Phase 1. |
|
||||||
|
| `apps/renderer/src/mini-apps/apps/twitter-client.ts` | The sample app: self-contained HTML (React+Tailwind+Babel CDN) + static `data`. |
|
||||||
|
| `apps/renderer/src/components/mini-apps-view.tsx` | Card grid; internal `selectedAppId` state; renders grid or open app. |
|
||||||
|
| `apps/renderer/src/components/mini-app-frame.tsx` | Sandboxed iframe (`srcdoc`) + `window.rowboat` postMessage bridge (data wired; actions stubbed → toast/log). |
|
||||||
|
|
||||||
|
The bridge message protocol (host ↔ iframe), defined once and shared:
|
||||||
|
- iframe → host: `{ type: 'rowboat:mini-app:ready' }`,
|
||||||
|
`{ type: 'rowboat:mini-app:action', id, scope, action, args }`,
|
||||||
|
`{ type: 'rowboat:mini-app:setState', patch }`
|
||||||
|
- host → iframe: `{ type: 'rowboat:mini-app:data', data }`,
|
||||||
|
`{ type: 'rowboat:mini-app:state', state }`,
|
||||||
|
`{ type: 'rowboat:mini-app:action-result', id, ok, result?, error? }`
|
||||||
|
|
||||||
|
### 3.2 Wiring into `App.tsx` (mirror the `bg-tasks` view exactly)
|
||||||
|
|
||||||
|
Add a first-class `apps` view. Edit sites (all mirror an existing view):
|
||||||
|
|
||||||
|
1. **Tab path const** (~L198): add `const APPS_TAB_PATH = '__rowboat_mini_apps__'`.
|
||||||
|
2. **Tab predicate** (~L374): `const isAppsTabPath = (path) => path === APPS_TAB_PATH`.
|
||||||
|
3. **ViewState union** (~L636): add `| { type: 'apps' }`.
|
||||||
|
4. **`parseDeepLink`** (~L713): add `case 'apps': return { type: 'apps' }`.
|
||||||
|
5. **State flag** (~L825): `const [isAppsOpen, setIsAppsOpen] = useState(false)`.
|
||||||
|
6. **Tab title** (~L1253): `if (isAppsTabPath(tab.path)) return 'Mini Apps'`.
|
||||||
|
7. **Tab activation → flag** (~L3269, ~L3443): set `isAppsOpen` from tab path.
|
||||||
|
8. **Closing-tab guard** (~L3376): add `&& !isAppsTabPath(closingTab.path)`.
|
||||||
|
9. **`currentViewState`** (~L3770): `if (isAppsOpen) return { type: 'apps' }`
|
||||||
|
(place alongside bg-tasks; add to deps array).
|
||||||
|
10. **`ensureAppsFileTab`** (~L3853): mirror `ensureBgTasksFileTab`.
|
||||||
|
11. **`openAppsView`** (~L3953): mirror `openBgTasksView`; reset all other flags,
|
||||||
|
`setIsAppsOpen(true)`, `ensureAppsFileTab()`.
|
||||||
|
12. **`applyViewState` switch** (~L4195): add `case 'apps':` mirroring `bg-tasks`.
|
||||||
|
13. **Add `setIsAppsOpen(false)`** to every *other* view's reset block (the
|
||||||
|
shared multi-`set...(false)` lines) so opening another view clears apps and
|
||||||
|
closing it doesn't fall back to apps. Use targeted edits per block.
|
||||||
|
14. **Derived booleans** that OR all view flags (isFullScreenChat,
|
||||||
|
rightPaneAvailable, inFileView, rightPaneContext, viewOpen, keyboard guards):
|
||||||
|
add `|| isAppsOpen` / `&& !isAppsOpen` consistently (search `isBgTasksOpen`).
|
||||||
|
15. **Tab-path mapping** (~L4668): add `: isAppsOpen ? APPS_TAB_PATH`.
|
||||||
|
16. **Render branch** (~L5894): add `) : isAppsOpen ? (<MiniAppsView ... />`.
|
||||||
|
|
||||||
|
### 3.3 Sidebar entry (`sidebar-content.tsx`)
|
||||||
|
|
||||||
|
- Add `onOpenApps?: () => void` prop (~L163) and destructure (~L412).
|
||||||
|
- Add a `SidebarMenuButton` with `isActive={activeNav === 'apps'}` and a
|
||||||
|
`LayoutGrid` (lucide) icon, label "Mini Apps", in the lower nav group near
|
||||||
|
"Background agents" (~L830).
|
||||||
|
- Thread `activeNav === 'apps'` from the parent (App.tsx passes `activeNav` based
|
||||||
|
on `isAppsOpen`, mirroring how `'agents'` is derived ~L5651).
|
||||||
|
- Wire `onOpenApps={openAppsView}` at the `SidebarContent`/home call sites
|
||||||
|
(~L5657, ~L5832).
|
||||||
|
|
||||||
|
### 3.4 The sample app (twitter-client)
|
||||||
|
|
||||||
|
Self-contained HTML string. React + ReactDOM + Babel-standalone + Tailwind, all
|
||||||
|
via CDN. Renders three sections — **To read / To repost / To respond** (reply
|
||||||
|
pre-drafted) — from data delivered via `rowboat.onData`. Buttons call
|
||||||
|
`rowboat.callAction('twitter', ...)`; in Phase 1 the host just toasts/logs and
|
||||||
|
returns a fake ok. Static `data` lives next to the HTML.
|
||||||
|
|
||||||
|
> Note: CDN scripts require network + `allow-scripts` in the sandbox. The frame
|
||||||
|
> uses `sandbox="allow-scripts allow-popups allow-forms allow-modals"` — **no**
|
||||||
|
> `allow-same-origin` (keeps the app from reaching host cookies/storage; the
|
||||||
|
> bridge is the only channel). Verify CDN loads under this sandbox during testing;
|
||||||
|
> if blocked, fall back to bundling the libs locally as a renderer asset.
|
||||||
|
|
||||||
|
### 3.5 Verify
|
||||||
|
|
||||||
|
- `cd apps/x && npm run deps && npm run lint` compiles clean.
|
||||||
|
- `npm run dev`: "Mini Apps" appears in the sidebar; opens the card grid; clicking
|
||||||
|
the Twitter card renders the app full-screen in the pane; sections populate from
|
||||||
|
data; clicking a button shows the stubbed action result; tabs/back/forward and
|
||||||
|
switching to other views behave like every other view.
|
||||||
|
|
||||||
|
## 4. Out of scope for Phase 1 (later phases)
|
||||||
|
|
||||||
|
`~/.rowboat/apps/<slug>/` storage + IPC; the agent backend (reuse bg-tasks
|
||||||
|
engine); real Composio execution through the bridge; per-app scope enforcement &
|
||||||
|
auth prompting; generation via Copilot/Code-Mode; persistent state store;
|
||||||
|
conversational editing & error recovery; the V2 workspace drag.
|
||||||
|
|
@ -27,6 +27,7 @@ import { SidebarContentPanel } from '@/components/sidebar-content';
|
||||||
import { SuggestedTopicsView } from '@/components/suggested-topics-view';
|
import { SuggestedTopicsView } from '@/components/suggested-topics-view';
|
||||||
import { LiveNotesView } from '@/components/live-notes-view';
|
import { LiveNotesView } from '@/components/live-notes-view';
|
||||||
import { BgTasksView } from '@/components/bg-tasks-view';
|
import { BgTasksView } from '@/components/bg-tasks-view';
|
||||||
|
import { MiniAppsView } from '@/components/mini-apps-view';
|
||||||
import { EmailView } from '@/components/email-view';
|
import { EmailView } from '@/components/email-view';
|
||||||
import { WorkspaceView } from '@/components/workspace-view';
|
import { WorkspaceView } from '@/components/workspace-view';
|
||||||
import { CodingRunBlock } from '@/components/coding-run';
|
import { CodingRunBlock } from '@/components/coding-run';
|
||||||
|
|
@ -196,6 +197,7 @@ const SUGGESTED_TOPICS_TAB_PATH = '__rowboat_suggested_topics__'
|
||||||
const MEETINGS_TAB_PATH = '__rowboat_meetings__'
|
const MEETINGS_TAB_PATH = '__rowboat_meetings__'
|
||||||
const LIVE_NOTES_TAB_PATH = '__rowboat_live_notes__'
|
const LIVE_NOTES_TAB_PATH = '__rowboat_live_notes__'
|
||||||
const BG_TASKS_TAB_PATH = '__rowboat_bg_tasks__'
|
const BG_TASKS_TAB_PATH = '__rowboat_bg_tasks__'
|
||||||
|
const APPS_TAB_PATH = '__rowboat_mini_apps__'
|
||||||
const EMAIL_TAB_PATH = '__rowboat_email__'
|
const EMAIL_TAB_PATH = '__rowboat_email__'
|
||||||
const WORKSPACE_TAB_PATH = '__rowboat_workspace__'
|
const WORKSPACE_TAB_PATH = '__rowboat_workspace__'
|
||||||
const WORKSPACE_ROOT = 'knowledge/Workspace'
|
const WORKSPACE_ROOT = 'knowledge/Workspace'
|
||||||
|
|
@ -372,6 +374,7 @@ const isSuggestedTopicsTabPath = (path: string) => path === SUGGESTED_TOPICS_TAB
|
||||||
const isMeetingsTabPath = (path: string) => path === MEETINGS_TAB_PATH
|
const isMeetingsTabPath = (path: string) => path === MEETINGS_TAB_PATH
|
||||||
const isLiveNotesTabPath = (path: string) => path === LIVE_NOTES_TAB_PATH
|
const isLiveNotesTabPath = (path: string) => path === LIVE_NOTES_TAB_PATH
|
||||||
const isBgTasksTabPath = (path: string) => path === BG_TASKS_TAB_PATH
|
const isBgTasksTabPath = (path: string) => path === BG_TASKS_TAB_PATH
|
||||||
|
const isAppsTabPath = (path: string) => path === APPS_TAB_PATH
|
||||||
const isEmailTabPath = (path: string) => path === EMAIL_TAB_PATH
|
const isEmailTabPath = (path: string) => path === EMAIL_TAB_PATH
|
||||||
const isWorkspaceTabPath = (path: string) => path === WORKSPACE_TAB_PATH
|
const isWorkspaceTabPath = (path: string) => path === WORKSPACE_TAB_PATH
|
||||||
const isKnowledgeViewTabPath = (path: string) => path === KNOWLEDGE_VIEW_TAB_PATH
|
const isKnowledgeViewTabPath = (path: string) => path === KNOWLEDGE_VIEW_TAB_PATH
|
||||||
|
|
@ -634,6 +637,7 @@ type ViewState =
|
||||||
| { type: 'home' }
|
| { type: 'home' }
|
||||||
| { type: 'code' }
|
| { type: 'code' }
|
||||||
| { type: 'bg-tasks' }
|
| { type: 'bg-tasks' }
|
||||||
|
| { type: 'apps' }
|
||||||
|
|
||||||
function viewStatesEqual(a: ViewState, b: ViewState): boolean {
|
function viewStatesEqual(a: ViewState, b: ViewState): boolean {
|
||||||
if (a.type !== b.type) return false
|
if (a.type !== b.type) return false
|
||||||
|
|
@ -712,6 +716,8 @@ function parseDeepLink(input: string): ViewState | null {
|
||||||
return { type: 'code' }
|
return { type: 'code' }
|
||||||
case 'bg-tasks':
|
case 'bg-tasks':
|
||||||
return { type: 'bg-tasks' }
|
return { type: 'bg-tasks' }
|
||||||
|
case 'apps':
|
||||||
|
return { type: 'apps' }
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -823,6 +829,7 @@ function App() {
|
||||||
const [isMeetingsOpen, setIsMeetingsOpen] = useState(false)
|
const [isMeetingsOpen, setIsMeetingsOpen] = useState(false)
|
||||||
const [isLiveNotesOpen, setIsLiveNotesOpen] = useState(false)
|
const [isLiveNotesOpen, setIsLiveNotesOpen] = useState(false)
|
||||||
const [isBgTasksOpen, setIsBgTasksOpen] = useState(false)
|
const [isBgTasksOpen, setIsBgTasksOpen] = useState(false)
|
||||||
|
const [isAppsOpen, setIsAppsOpen] = useState(false)
|
||||||
const [isEmailOpen, setIsEmailOpen] = useState(false)
|
const [isEmailOpen, setIsEmailOpen] = useState(false)
|
||||||
const [isWorkspaceOpen, setIsWorkspaceOpen] = useState(false)
|
const [isWorkspaceOpen, setIsWorkspaceOpen] = useState(false)
|
||||||
const [workspaceInitialPath, setWorkspaceInitialPath] = useState<string | null>(null)
|
const [workspaceInitialPath, setWorkspaceInitialPath] = useState<string | null>(null)
|
||||||
|
|
@ -1251,6 +1258,7 @@ function App() {
|
||||||
if (isMeetingsTabPath(tab.path)) return 'Meetings'
|
if (isMeetingsTabPath(tab.path)) return 'Meetings'
|
||||||
if (isLiveNotesTabPath(tab.path)) return 'Live notes'
|
if (isLiveNotesTabPath(tab.path)) return 'Live notes'
|
||||||
if (isBgTasksTabPath(tab.path)) return 'Background tasks'
|
if (isBgTasksTabPath(tab.path)) return 'Background tasks'
|
||||||
|
if (isAppsTabPath(tab.path)) return 'Mini Apps'
|
||||||
if (isEmailTabPath(tab.path)) return 'Email'
|
if (isEmailTabPath(tab.path)) return 'Email'
|
||||||
if (isWorkspaceTabPath(tab.path)) return 'Workspace'
|
if (isWorkspaceTabPath(tab.path)) return 'Workspace'
|
||||||
if (isKnowledgeViewTabPath(tab.path)) return 'Brain'
|
if (isKnowledgeViewTabPath(tab.path)) return 'Brain'
|
||||||
|
|
@ -3214,7 +3222,7 @@ function App() {
|
||||||
setActiveFileTabId(existingTab.id)
|
setActiveFileTabId(existingTab.id)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setSelectedPath(path)
|
setSelectedPath(path)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -3223,7 +3231,7 @@ function App() {
|
||||||
setActiveFileTabId(id)
|
setActiveFileTabId(id)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setSelectedPath(path)
|
setSelectedPath(path)
|
||||||
}, [fileTabs, dismissBrowserOverlay])
|
}, [fileTabs, dismissBrowserOverlay])
|
||||||
|
|
||||||
|
|
@ -3242,14 +3250,14 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(true)
|
setIsGraphOpen(true)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isSuggestedTopicsTabPath(tab.path)) {
|
if (isSuggestedTopicsTabPath(tab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(true)
|
setIsSuggestedTopicsOpen(true)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isLiveNotesTabPath(tab.path)) {
|
if (isLiveNotesTabPath(tab.path)) {
|
||||||
|
|
@ -3262,7 +3270,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setIsLiveNotesOpen(true)
|
setIsLiveNotesOpen(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -3276,10 +3284,25 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setIsBgTasksOpen(true)
|
setIsBgTasksOpen(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (isAppsTabPath(tab.path)) {
|
||||||
|
setSelectedPath(null)
|
||||||
|
setIsGraphOpen(false)
|
||||||
|
setIsSuggestedTopicsOpen(false)
|
||||||
|
setIsMeetingsOpen(false)
|
||||||
|
setIsLiveNotesOpen(false)
|
||||||
|
setIsBgTasksOpen(false)
|
||||||
|
setIsEmailOpen(false)
|
||||||
|
setIsWorkspaceOpen(false)
|
||||||
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
|
setIsHomeOpen(false)
|
||||||
|
setIsAppsOpen(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (isMeetingsTabPath(tab.path)) {
|
if (isMeetingsTabPath(tab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
|
|
@ -3291,7 +3314,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isEmailTabPath(tab.path)) {
|
if (isEmailTabPath(tab.path)) {
|
||||||
|
|
@ -3304,7 +3327,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setIsEmailOpen(true)
|
setIsEmailOpen(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -3318,7 +3341,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setIsWorkspaceOpen(true)
|
setIsWorkspaceOpen(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -3332,7 +3355,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setIsKnowledgeViewOpen(true)
|
setIsKnowledgeViewOpen(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -3346,7 +3369,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(true); setIsHomeOpen(false)
|
setIsChatHistoryOpen(true); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isHomeTabPath(tab.path)) {
|
if (isHomeTabPath(tab.path)) {
|
||||||
|
|
@ -3354,7 +3377,7 @@ function App() {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(true)
|
setIsHomeOpen(true); setIsAppsOpen(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isCodeTabPath(tab.path)) {
|
if (isCodeTabPath(tab.path)) {
|
||||||
|
|
@ -3362,18 +3385,18 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setSelectedPath(tab.path)
|
setSelectedPath(tab.path)
|
||||||
}, [fileTabs, isRightPaneMaximized, dismissBrowserOverlay])
|
}, [fileTabs, isRightPaneMaximized, dismissBrowserOverlay])
|
||||||
|
|
||||||
const closeFileTab = useCallback((tabId: string) => {
|
const closeFileTab = useCallback((tabId: string) => {
|
||||||
const closingTab = fileTabs.find(t => t.id === tabId)
|
const closingTab = fileTabs.find(t => t.id === tabId)
|
||||||
if (closingTab && !isGraphTabPath(closingTab.path) && !isSuggestedTopicsTabPath(closingTab.path) && !isLiveNotesTabPath(closingTab.path) && !isBgTasksTabPath(closingTab.path) && !isEmailTabPath(closingTab.path) && !isWorkspaceTabPath(closingTab.path) && !isKnowledgeViewTabPath(closingTab.path) && !isChatHistoryTabPath(closingTab.path) && !isHomeTabPath(closingTab.path) && !isCodeTabPath(closingTab.path) && !isBaseFilePath(closingTab.path)) {
|
if (closingTab && !isGraphTabPath(closingTab.path) && !isSuggestedTopicsTabPath(closingTab.path) && !isLiveNotesTabPath(closingTab.path) && !isBgTasksTabPath(closingTab.path) && !isAppsTabPath(closingTab.path) && !isEmailTabPath(closingTab.path) && !isWorkspaceTabPath(closingTab.path) && !isKnowledgeViewTabPath(closingTab.path) && !isChatHistoryTabPath(closingTab.path) && !isHomeTabPath(closingTab.path) && !isCodeTabPath(closingTab.path) && !isBaseFilePath(closingTab.path)) {
|
||||||
removeEditorCacheForPath(closingTab.path)
|
removeEditorCacheForPath(closingTab.path)
|
||||||
initialContentByPathRef.current.delete(closingTab.path)
|
initialContentByPathRef.current.delete(closingTab.path)
|
||||||
untitledRenameReadyPathsRef.current.delete(closingTab.path)
|
untitledRenameReadyPathsRef.current.delete(closingTab.path)
|
||||||
|
|
@ -3396,7 +3419,7 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const idx = prev.findIndex(t => t.id === tabId)
|
const idx = prev.findIndex(t => t.id === tabId)
|
||||||
|
|
@ -3410,12 +3433,12 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(true)
|
setIsGraphOpen(true)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
} else if (isSuggestedTopicsTabPath(newActiveTab.path)) {
|
} else if (isSuggestedTopicsTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(true)
|
setIsSuggestedTopicsOpen(true)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
} else if (isMeetingsTabPath(newActiveTab.path)) {
|
} else if (isMeetingsTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
|
|
@ -3427,7 +3450,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
} else if (isLiveNotesTabPath(newActiveTab.path)) {
|
} else if (isLiveNotesTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
|
|
@ -3438,7 +3461,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setIsLiveNotesOpen(true)
|
setIsLiveNotesOpen(true)
|
||||||
} else if (isBgTasksTabPath(newActiveTab.path)) {
|
} else if (isBgTasksTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
|
|
@ -3451,7 +3474,20 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
|
} else if (isAppsTabPath(newActiveTab.path)) {
|
||||||
|
setSelectedPath(null)
|
||||||
|
setIsGraphOpen(false)
|
||||||
|
setIsSuggestedTopicsOpen(false)
|
||||||
|
setIsMeetingsOpen(false)
|
||||||
|
setIsLiveNotesOpen(false)
|
||||||
|
setIsBgTasksOpen(false)
|
||||||
|
setIsEmailOpen(false)
|
||||||
|
setIsWorkspaceOpen(false)
|
||||||
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
|
setIsHomeOpen(false)
|
||||||
|
setIsAppsOpen(true)
|
||||||
} else if (isEmailTabPath(newActiveTab.path)) {
|
} else if (isEmailTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
|
|
@ -3462,7 +3498,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setIsEmailOpen(true)
|
setIsEmailOpen(true)
|
||||||
} else if (isWorkspaceTabPath(newActiveTab.path)) {
|
} else if (isWorkspaceTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
|
|
@ -3474,7 +3510,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setIsWorkspaceOpen(true)
|
setIsWorkspaceOpen(true)
|
||||||
} else if (isKnowledgeViewTabPath(newActiveTab.path)) {
|
} else if (isKnowledgeViewTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
|
|
@ -3486,7 +3522,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setIsKnowledgeViewOpen(true)
|
setIsKnowledgeViewOpen(true)
|
||||||
} else if (isChatHistoryTabPath(newActiveTab.path)) {
|
} else if (isChatHistoryTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
|
|
@ -3498,17 +3534,17 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(true); setIsHomeOpen(false)
|
setIsChatHistoryOpen(true); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
} else if (isHomeTabPath(newActiveTab.path)) {
|
} else if (isHomeTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(true)
|
setIsHomeOpen(true); setIsAppsOpen(false)
|
||||||
} else {
|
} else {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setSelectedPath(newActiveTab.path)
|
setSelectedPath(newActiveTab.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3530,7 +3566,7 @@ function App() {
|
||||||
dismissBrowserOverlay()
|
dismissBrowserOverlay()
|
||||||
handleNewChat()
|
handleNewChat()
|
||||||
// Left-pane "new chat" should always open full chat view.
|
// Left-pane "new chat" should always open full chat view.
|
||||||
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) {
|
||||||
setExpandedFrom({
|
setExpandedFrom({
|
||||||
path: selectedPath,
|
path: selectedPath,
|
||||||
graph: isGraphOpen,
|
graph: isGraphOpen,
|
||||||
|
|
@ -3547,8 +3583,8 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
}, [dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, isHomeOpen])
|
}, [dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isAppsOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, isHomeOpen])
|
||||||
|
|
||||||
// Sidebar variant: reset the chat in place without leaving file/graph context.
|
// Sidebar variant: reset the chat in place without leaving file/graph context.
|
||||||
const handleNewChatTabInSidebar = useCallback(() => {
|
const handleNewChatTabInSidebar = useCallback(() => {
|
||||||
|
|
@ -3680,7 +3716,7 @@ function App() {
|
||||||
|
|
||||||
const handleOpenFullScreenChat = useCallback(() => {
|
const handleOpenFullScreenChat = useCallback(() => {
|
||||||
// Remember where we came from so the close button can return
|
// Remember where we came from so the close button can return
|
||||||
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) {
|
||||||
setExpandedFrom({
|
setExpandedFrom({
|
||||||
path: selectedPath,
|
path: selectedPath,
|
||||||
graph: isGraphOpen,
|
graph: isGraphOpen,
|
||||||
|
|
@ -3696,8 +3732,8 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
}, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, dismissBrowserOverlay])
|
}, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isAppsOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, dismissBrowserOverlay])
|
||||||
|
|
||||||
const handleCloseFullScreenChat = useCallback((): boolean => {
|
const handleCloseFullScreenChat = useCallback((): boolean => {
|
||||||
let restored = false
|
let restored = false
|
||||||
|
|
@ -3706,11 +3742,11 @@ function App() {
|
||||||
if (expandedFrom.graph) {
|
if (expandedFrom.graph) {
|
||||||
setIsGraphOpen(true)
|
setIsGraphOpen(true)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
} else if (expandedFrom.suggestedTopics) {
|
} else if (expandedFrom.suggestedTopics) {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(true)
|
setIsSuggestedTopicsOpen(true)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
} else if (expandedFrom.meetings) {
|
} else if (expandedFrom.meetings) {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
|
|
@ -3742,7 +3778,7 @@ function App() {
|
||||||
} else if (expandedFrom.path) {
|
} else if (expandedFrom.path) {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setSelectedPath(expandedFrom.path)
|
setSelectedPath(expandedFrom.path)
|
||||||
} else {
|
} else {
|
||||||
// expandedFrom was captured from a view this restorer doesn't track
|
// expandedFrom was captured from a view this restorer doesn't track
|
||||||
|
|
@ -3768,10 +3804,11 @@ function App() {
|
||||||
if (isHomeOpen) return { type: 'home' }
|
if (isHomeOpen) return { type: 'home' }
|
||||||
if (isCodeOpen) return { type: 'code' }
|
if (isCodeOpen) return { type: 'code' }
|
||||||
if (isBgTasksOpen) return { type: 'bg-tasks' }
|
if (isBgTasksOpen) return { type: 'bg-tasks' }
|
||||||
|
if (isAppsOpen) return { type: 'apps' }
|
||||||
if (selectedPath) return { type: 'file', path: selectedPath }
|
if (selectedPath) return { type: 'file', path: selectedPath }
|
||||||
if (isGraphOpen) return { type: 'graph' }
|
if (isGraphOpen) return { type: 'graph' }
|
||||||
return { type: 'chat', runId }
|
return { type: 'chat', runId }
|
||||||
}, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, isWorkspaceOpen, isKnowledgeViewOpen, knowledgeViewFolderPath, knowledgeViewMode, isChatHistoryOpen, isHomeOpen, isCodeOpen, workspaceInitialPath, runId])
|
}, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isAppsOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, isWorkspaceOpen, isKnowledgeViewOpen, knowledgeViewFolderPath, knowledgeViewMode, isChatHistoryOpen, isHomeOpen, isCodeOpen, workspaceInitialPath, runId])
|
||||||
|
|
||||||
const appendUnique = useCallback((stack: ViewState[], entry: ViewState) => {
|
const appendUnique = useCallback((stack: ViewState[], entry: ViewState) => {
|
||||||
const last = stack[stack.length - 1]
|
const last = stack[stack.length - 1]
|
||||||
|
|
@ -3861,6 +3898,17 @@ function App() {
|
||||||
setActiveFileTabId(id)
|
setActiveFileTabId(id)
|
||||||
}, [fileTabs])
|
}, [fileTabs])
|
||||||
|
|
||||||
|
const ensureAppsFileTab = useCallback(() => {
|
||||||
|
const existing = fileTabs.find((tab) => isAppsTabPath(tab.path))
|
||||||
|
if (existing) {
|
||||||
|
setActiveFileTabId(existing.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const id = newFileTabId()
|
||||||
|
setFileTabs((prev) => [...prev, { id, path: APPS_TAB_PATH }])
|
||||||
|
setActiveFileTabId(id)
|
||||||
|
}, [fileTabs])
|
||||||
|
|
||||||
const ensureEmailFileTab = useCallback(() => {
|
const ensureEmailFileTab = useCallback(() => {
|
||||||
const existing = fileTabs.find((tab) => isEmailTabPath(tab.path))
|
const existing = fileTabs.find((tab) => isEmailTabPath(tab.path))
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
|
@ -3938,7 +3986,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
|
|
@ -3955,7 +4003,7 @@ function App() {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsBrowserOpen(false)
|
setIsBrowserOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
|
|
@ -3963,6 +4011,19 @@ function App() {
|
||||||
ensureBgTasksFileTab()
|
ensureBgTasksFileTab()
|
||||||
}, [ensureBgTasksFileTab])
|
}, [ensureBgTasksFileTab])
|
||||||
|
|
||||||
|
const openAppsView = useCallback(() => {
|
||||||
|
setSelectedPath(null)
|
||||||
|
setIsGraphOpen(false)
|
||||||
|
setIsBrowserOpen(false)
|
||||||
|
setIsSuggestedTopicsOpen(false)
|
||||||
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
||||||
|
setSelectedBackgroundTask(null)
|
||||||
|
setExpandedFrom(null)
|
||||||
|
setIsRightPaneMaximized(false)
|
||||||
|
setIsAppsOpen(true)
|
||||||
|
ensureAppsFileTab()
|
||||||
|
}, [ensureAppsFileTab])
|
||||||
|
|
||||||
const openMeetingsView = useCallback(() => {
|
const openMeetingsView = useCallback(() => {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
|
|
@ -3975,7 +4036,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
|
|
@ -3987,7 +4048,7 @@ function App() {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsBrowserOpen(false)
|
setIsBrowserOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
|
|
@ -4003,7 +4064,7 @@ function App() {
|
||||||
// visible in the middle pane.
|
// visible in the middle pane.
|
||||||
setIsBrowserOpen(false)
|
setIsBrowserOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
// Preserve split vs knowledge-max mode when navigating knowledge files.
|
// Preserve split vs knowledge-max mode when navigating knowledge files.
|
||||||
// Only exit chat-only maximize, because that would hide the selected file.
|
// Only exit chat-only maximize, because that would hide the selected file.
|
||||||
|
|
@ -4018,7 +4079,7 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsBrowserOpen(false)
|
setIsBrowserOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
setIsGraphOpen(true)
|
setIsGraphOpen(true)
|
||||||
ensureGraphFileTab()
|
ensureGraphFileTab()
|
||||||
|
|
@ -4031,7 +4092,7 @@ function App() {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsBrowserOpen(false)
|
setIsBrowserOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
setSelectedBackgroundTask(view.name)
|
setSelectedBackgroundTask(view.name)
|
||||||
|
|
@ -4044,7 +4105,7 @@ function App() {
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
setIsSuggestedTopicsOpen(true)
|
setIsSuggestedTopicsOpen(true)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
ensureSuggestedTopicsFileTab()
|
ensureSuggestedTopicsFileTab()
|
||||||
return
|
return
|
||||||
case 'meetings':
|
case 'meetings':
|
||||||
|
|
@ -4062,7 +4123,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
ensureMeetingsFileTab()
|
ensureMeetingsFileTab()
|
||||||
return
|
return
|
||||||
case 'live-notes':
|
case 'live-notes':
|
||||||
|
|
@ -4079,7 +4140,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setIsLiveNotesOpen(true)
|
setIsLiveNotesOpen(true)
|
||||||
ensureLiveNotesFileTab()
|
ensureLiveNotesFileTab()
|
||||||
return
|
return
|
||||||
|
|
@ -4098,7 +4159,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
// Deep links (e.g. a new-email notification) carry the thread to open;
|
// Deep links (e.g. a new-email notification) carry the thread to open;
|
||||||
// bump the version so EmailView re-selects it even if email is already open.
|
// bump the version so EmailView re-selects it even if email is already open.
|
||||||
if (view.threadId) {
|
if (view.threadId) {
|
||||||
|
|
@ -4122,7 +4183,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(true)
|
setIsWorkspaceOpen(true)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setWorkspaceInitialPath(view.path ?? null)
|
setWorkspaceInitialPath(view.path ?? null)
|
||||||
ensureWorkspaceFileTab()
|
ensureWorkspaceFileTab()
|
||||||
return
|
return
|
||||||
|
|
@ -4143,7 +4204,7 @@ function App() {
|
||||||
setKnowledgeViewMode(view.mode ?? (view.folderPath ? 'files' : 'graph'))
|
setKnowledgeViewMode(view.mode ?? (view.folderPath ? 'files' : 'graph'))
|
||||||
setKnowledgeViewFolderPath(view.folderPath ?? null)
|
setKnowledgeViewFolderPath(view.folderPath ?? null)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(false)
|
setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
ensureKnowledgeViewFileTab()
|
ensureKnowledgeViewFileTab()
|
||||||
return
|
return
|
||||||
case 'chat-history':
|
case 'chat-history':
|
||||||
|
|
@ -4160,7 +4221,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(true); setIsHomeOpen(false)
|
setIsChatHistoryOpen(true); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
ensureChatHistoryFileTab()
|
ensureChatHistoryFileTab()
|
||||||
return
|
return
|
||||||
case 'home':
|
case 'home':
|
||||||
|
|
@ -4178,7 +4239,7 @@ function App() {
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
setIsChatHistoryOpen(false)
|
setIsChatHistoryOpen(false)
|
||||||
setIsHomeOpen(true)
|
setIsHomeOpen(true); setIsAppsOpen(false)
|
||||||
ensureHomeFileTab()
|
ensureHomeFileTab()
|
||||||
return
|
return
|
||||||
case 'code':
|
case 'code':
|
||||||
|
|
@ -4189,7 +4250,7 @@ function App() {
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
ensureCodeFileTab()
|
ensureCodeFileTab()
|
||||||
return
|
return
|
||||||
case 'bg-tasks':
|
case 'bg-tasks':
|
||||||
|
|
@ -4200,10 +4261,22 @@ function App() {
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
setIsBgTasksOpen(true)
|
setIsBgTasksOpen(true)
|
||||||
ensureBgTasksFileTab()
|
ensureBgTasksFileTab()
|
||||||
return
|
return
|
||||||
|
case 'apps':
|
||||||
|
setSelectedPath(null)
|
||||||
|
setIsGraphOpen(false)
|
||||||
|
setIsBrowserOpen(false)
|
||||||
|
setExpandedFrom(null)
|
||||||
|
setIsRightPaneMaximized(false)
|
||||||
|
setSelectedBackgroundTask(null)
|
||||||
|
setIsSuggestedTopicsOpen(false)
|
||||||
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
||||||
|
setIsAppsOpen(true)
|
||||||
|
ensureAppsFileTab()
|
||||||
|
return
|
||||||
case 'chat':
|
case 'chat':
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
|
|
@ -4212,7 +4285,7 @@ function App() {
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false); setIsHomeOpen(false); setIsAppsOpen(false)
|
||||||
if (view.runId) {
|
if (view.runId) {
|
||||||
const targetRunId = view.runId
|
const targetRunId = view.runId
|
||||||
// Bind the loaded run to a chat tab so its title (derived from
|
// Bind the loaded run to a chat tab so its title (derived from
|
||||||
|
|
@ -4232,7 +4305,7 @@ function App() {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}, [ensureEmailFileTab, ensureMeetingsFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, ensureWorkspaceFileTab, ensureKnowledgeViewFileTab, ensureChatHistoryFileTab, ensureHomeFileTab, ensureCodeFileTab, ensureBgTasksFileTab, handleNewChat, isRightPaneMaximized, loadRun])
|
}, [ensureEmailFileTab, ensureMeetingsFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, ensureWorkspaceFileTab, ensureKnowledgeViewFileTab, ensureChatHistoryFileTab, ensureHomeFileTab, ensureCodeFileTab, ensureBgTasksFileTab, ensureAppsFileTab, handleNewChat, isRightPaneMaximized, loadRun])
|
||||||
|
|
||||||
const navigateToView = useCallback(async (nextView: ViewState) => {
|
const navigateToView = useCallback(async (nextView: ViewState) => {
|
||||||
const current = currentViewState
|
const current = currentViewState
|
||||||
|
|
@ -4566,7 +4639,7 @@ function App() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Keyboard shortcut: Ctrl+L to toggle main chat view
|
// Keyboard shortcut: Ctrl+L to toggle main chat view
|
||||||
const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isHomeOpen && !isCodeOpen && !selectedBackgroundTask && !isBrowserOpen
|
const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isHomeOpen && !isCodeOpen && !selectedBackgroundTask && !isBrowserOpen
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
|
||||||
|
|
@ -4651,11 +4724,11 @@ function App() {
|
||||||
const handleTabKeyDown = (e: KeyboardEvent) => {
|
const handleTabKeyDown = (e: KeyboardEvent) => {
|
||||||
const mod = e.metaKey || e.ctrlKey
|
const mod = e.metaKey || e.ctrlKey
|
||||||
if (!mod) return
|
if (!mod) return
|
||||||
const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) && isChatSidebarOpen)
|
const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen) && isChatSidebarOpen)
|
||||||
const targetPane: ShortcutPane = rightPaneAvailable
|
const targetPane: ShortcutPane = rightPaneAvailable
|
||||||
? (isRightPaneMaximized ? 'right' : activeShortcutPane)
|
? (isRightPaneMaximized ? 'right' : activeShortcutPane)
|
||||||
: 'left'
|
: 'left'
|
||||||
const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen)
|
const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen)
|
||||||
const selectedKnowledgePath = isGraphOpen
|
const selectedKnowledgePath = isGraphOpen
|
||||||
? GRAPH_TAB_PATH
|
? GRAPH_TAB_PATH
|
||||||
: isSuggestedTopicsOpen
|
: isSuggestedTopicsOpen
|
||||||
|
|
@ -4666,6 +4739,8 @@ function App() {
|
||||||
? LIVE_NOTES_TAB_PATH
|
? LIVE_NOTES_TAB_PATH
|
||||||
: isBgTasksOpen
|
: isBgTasksOpen
|
||||||
? BG_TASKS_TAB_PATH
|
? BG_TASKS_TAB_PATH
|
||||||
|
: isAppsOpen
|
||||||
|
? APPS_TAB_PATH
|
||||||
: isEmailOpen
|
: isEmailOpen
|
||||||
? EMAIL_TAB_PATH
|
? EMAIL_TAB_PATH
|
||||||
: isWorkspaceOpen
|
: isWorkspaceOpen
|
||||||
|
|
@ -4749,7 +4824,7 @@ function App() {
|
||||||
}
|
}
|
||||||
document.addEventListener('keydown', handleTabKeyDown)
|
document.addEventListener('keydown', handleTabKeyDown)
|
||||||
return () => document.removeEventListener('keydown', handleTabKeyDown)
|
return () => document.removeEventListener('keydown', handleTabKeyDown)
|
||||||
}, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
|
}, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isAppsOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
|
||||||
|
|
||||||
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
|
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
|
||||||
if (kind === 'file') {
|
if (kind === 'file') {
|
||||||
|
|
@ -4774,7 +4849,7 @@ function App() {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
||||||
setIsChatSidebarOpen(false)
|
setIsChatSidebarOpen(false)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
}
|
}
|
||||||
|
|
@ -4913,21 +4988,21 @@ function App() {
|
||||||
},
|
},
|
||||||
openGraph: () => {
|
openGraph: () => {
|
||||||
// From chat-only landing state, open graph directly in full knowledge view.
|
// From chat-only landing state, open graph directly in full knowledge view.
|
||||||
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
||||||
setIsChatSidebarOpen(false)
|
setIsChatSidebarOpen(false)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
}
|
}
|
||||||
void navigateToView({ type: 'graph' })
|
void navigateToView({ type: 'graph' })
|
||||||
},
|
},
|
||||||
openBases: () => {
|
openBases: () => {
|
||||||
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
||||||
setIsChatSidebarOpen(false)
|
setIsChatSidebarOpen(false)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
}
|
}
|
||||||
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
|
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
|
||||||
},
|
},
|
||||||
openWorkspaceAt: (path?: string) => {
|
openWorkspaceAt: (path?: string) => {
|
||||||
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
||||||
setIsChatSidebarOpen(false)
|
setIsChatSidebarOpen(false)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
}
|
}
|
||||||
|
|
@ -5597,7 +5672,7 @@ function App() {
|
||||||
const selectedTask = selectedBackgroundTask
|
const selectedTask = selectedBackgroundTask
|
||||||
? backgroundTasks.find(t => t.name === selectedBackgroundTask)
|
? backgroundTasks.find(t => t.name === selectedBackgroundTask)
|
||||||
: null
|
: null
|
||||||
const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isCodeOpen || isBrowserOpen)
|
const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isCodeOpen || isBrowserOpen)
|
||||||
const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized
|
const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized
|
||||||
const shouldCollapseLeftPane = isRightPaneOnlyMode
|
const shouldCollapseLeftPane = isRightPaneOnlyMode
|
||||||
const nonChatPaneStyle = React.useMemo<React.CSSProperties>(() => {
|
const nonChatPaneStyle = React.useMemo<React.CSSProperties>(() => {
|
||||||
|
|
@ -5645,7 +5720,7 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<SidebarSectionProvider defaultSection="tasks" onSectionChange={(section) => {
|
<SidebarSectionProvider defaultSection="tasks" onSectionChange={(section) => {
|
||||||
if (section === 'knowledge' && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isHomeOpen) {
|
if (section === 'knowledge' && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isHomeOpen) {
|
||||||
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
|
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
|
|
@ -5668,6 +5743,7 @@ function App() {
|
||||||
: isCodeOpen ? 'code'
|
: isCodeOpen ? 'code'
|
||||||
: (isKnowledgeViewOpen || isGraphOpen || (selectedPath != null && selectedPath.startsWith('knowledge/'))) ? 'knowledge'
|
: (isKnowledgeViewOpen || isGraphOpen || (selectedPath != null && selectedPath.startsWith('knowledge/'))) ? 'knowledge'
|
||||||
: isBgTasksOpen ? 'agents'
|
: isBgTasksOpen ? 'agents'
|
||||||
|
: isAppsOpen ? 'apps'
|
||||||
: isWorkspaceOpen ? 'workspaces'
|
: isWorkspaceOpen ? 'workspaces'
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
@ -5675,6 +5751,7 @@ function App() {
|
||||||
onOpenCode={openCodeView}
|
onOpenCode={openCodeView}
|
||||||
onOpenBgTasks={() => { setBgTaskInitialSlug(null); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }}
|
onOpenBgTasks={() => { setBgTaskInitialSlug(null); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }}
|
||||||
onOpenAgent={(slug) => { setBgTaskInitialSlug(slug); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }}
|
onOpenAgent={(slug) => { setBgTaskInitialSlug(slug); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }}
|
||||||
|
onOpenApps={openAppsView}
|
||||||
recentRuns={runs}
|
recentRuns={runs}
|
||||||
onOpenRun={(rid) => void navigateToView({ type: 'chat', runId: rid })}
|
onOpenRun={(rid) => void navigateToView({ type: 'chat', runId: rid })}
|
||||||
onOpenChatHistory={() => void navigateToView({ type: 'chat-history' })}
|
onOpenChatHistory={() => void navigateToView({ type: 'chat-history' })}
|
||||||
|
|
@ -5707,7 +5784,7 @@ function App() {
|
||||||
canNavigateForward={canNavigateForward}
|
canNavigateForward={canNavigateForward}
|
||||||
collapsedLeftPaddingPx={collapsedLeftPaddingPx}
|
collapsedLeftPaddingPx={collapsedLeftPaddingPx}
|
||||||
>
|
>
|
||||||
{(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isCodeOpen) && fileTabs.length >= 1 ? (
|
{(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isCodeOpen) && fileTabs.length >= 1 ? (
|
||||||
<TabBar
|
<TabBar
|
||||||
tabs={fileTabs}
|
tabs={fileTabs}
|
||||||
activeTabId={activeFileTabId ?? ''}
|
activeTabId={activeFileTabId ?? ''}
|
||||||
|
|
@ -5715,7 +5792,7 @@ function App() {
|
||||||
getTabId={(t) => t.id}
|
getTabId={(t) => t.id}
|
||||||
onSwitchTab={switchFileTab}
|
onSwitchTab={switchFileTab}
|
||||||
onCloseTab={closeFileTab}
|
onCloseTab={closeFileTab}
|
||||||
allowSingleTabClose={fileTabs.length === 1 && (isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isCodeOpen || (selectedPath != null && isBaseFilePath(selectedPath)))}
|
allowSingleTabClose={fileTabs.length === 1 && (isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isCodeOpen || (selectedPath != null && isBaseFilePath(selectedPath)))}
|
||||||
/>
|
/>
|
||||||
) : isFullScreenChat ? (
|
) : isFullScreenChat ? (
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
|
|
@ -5780,7 +5857,7 @@ function App() {
|
||||||
<TooltipContent side="bottom">Version history</TooltipContent>
|
<TooltipContent side="bottom">Version history</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{!isFullScreenChat && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isCodeOpen && !selectedTask && !isBrowserOpen && (
|
{!isFullScreenChat && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isAppsOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isCodeOpen && !selectedTask && !isBrowserOpen && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|
@ -5800,7 +5877,7 @@ function App() {
|
||||||
a freshly-mounted no-drag button inside the drag-region header
|
a freshly-mounted no-drag button inside the drag-region header
|
||||||
otherwise has its first click swallowed by the window drag. */}
|
otherwise has its first click swallowed by the window drag. */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const viewOpen = selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen
|
const viewOpen = selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isAppsOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen
|
||||||
const action = isFullScreenChat
|
const action = isFullScreenChat
|
||||||
? { onClick: pushChatToSidePane, icon: <ArrowRight className="size-5" />, label: 'Dock chat to side pane' }
|
? { onClick: pushChatToSidePane, icon: <ArrowRight className="size-5" />, label: 'Dock chat to side pane' }
|
||||||
: (viewOpen && !isChatSidebarOpen)
|
: (viewOpen && !isChatSidebarOpen)
|
||||||
|
|
@ -5904,6 +5981,10 @@ function App() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : isAppsOpen ? (
|
||||||
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
|
<MiniAppsView />
|
||||||
|
</div>
|
||||||
) : isEmailOpen ? (
|
) : isEmailOpen ? (
|
||||||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
<EmailView initialThreadId={emailInitialThreadId} threadIdVersion={emailThreadIdVersion} />
|
<EmailView initialThreadId={emailInitialThreadId} threadIdVersion={emailThreadIdVersion} />
|
||||||
|
|
|
||||||
96
apps/x/apps/renderer/src/components/mini-app-frame.tsx
Normal file
96
apps/x/apps/renderer/src/components/mini-app-frame.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import type { MiniApp, MiniAppOutboundMessage } from '@/mini-apps/types'
|
||||||
|
import { MINI_APP_MESSAGE } from '@/mini-apps/types'
|
||||||
|
|
||||||
|
// Host side of the Mini App bridge. Renders the app's self-contained HTML in a
|
||||||
|
// sandboxed iframe and answers the postMessage protocol in mini-apps/types.ts.
|
||||||
|
//
|
||||||
|
// Phase 1: data is delivered from the static app.data, per-app state lives in
|
||||||
|
// memory only, and callAction is stubbed (returns a friendly demo result). Later
|
||||||
|
// phases replace the action/state handlers with real IPC to the agent + Composio.
|
||||||
|
|
||||||
|
// Sandbox intentionally omits allow-same-origin: the app gets an opaque origin so
|
||||||
|
// it cannot reach host cookies/storage. The bridge is the only channel out.
|
||||||
|
const SANDBOX = 'allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals allow-downloads'
|
||||||
|
|
||||||
|
// Phase 1 stub: pretend the Composio action succeeded.
|
||||||
|
function stubActionResult(action: string): { message: string } {
|
||||||
|
switch (action) {
|
||||||
|
case 'repost': return { message: 'Reposted (demo)' }
|
||||||
|
case 'reply': return { message: 'Reply sent (demo)' }
|
||||||
|
case 'mark_read': return { message: 'Dismissed' }
|
||||||
|
default: return { message: action + ' done (demo)' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MiniAppFrame({ app }: { app: MiniApp }) {
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement | null>(null)
|
||||||
|
// In-memory per-app state for Phase 1 (resets when the frame unmounts).
|
||||||
|
const stateRef = useRef<Record<string, unknown>>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset state when switching apps.
|
||||||
|
stateRef.current = {}
|
||||||
|
|
||||||
|
function postToFrame(message: unknown) {
|
||||||
|
iframeRef.current?.contentWindow?.postMessage(message, '*')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessage(event: MessageEvent) {
|
||||||
|
const frameWindow = iframeRef.current?.contentWindow
|
||||||
|
if (!frameWindow || event.source !== frameWindow) return
|
||||||
|
|
||||||
|
const msg = event.data as MiniAppOutboundMessage
|
||||||
|
if (!msg || typeof msg !== 'object') return
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case MINI_APP_MESSAGE.ready: {
|
||||||
|
postToFrame({ type: MINI_APP_MESSAGE.data, data: app.data })
|
||||||
|
postToFrame({ type: MINI_APP_MESSAGE.state, state: stateRef.current })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case MINI_APP_MESSAGE.action: {
|
||||||
|
// Phase 1: stubbed. (Phase 2 enforces app.scope and calls Composio.)
|
||||||
|
const allowed = app.scope.includes(msg.scope)
|
||||||
|
if (!allowed) {
|
||||||
|
postToFrame({
|
||||||
|
type: MINI_APP_MESSAGE.actionResult,
|
||||||
|
id: msg.id,
|
||||||
|
ok: false,
|
||||||
|
error: 'Action scope "' + msg.scope + '" not granted to this app',
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Small delay so the UI's busy state is visible.
|
||||||
|
window.setTimeout(() => {
|
||||||
|
postToFrame({
|
||||||
|
type: MINI_APP_MESSAGE.actionResult,
|
||||||
|
id: msg.id,
|
||||||
|
ok: true,
|
||||||
|
result: stubActionResult(msg.action),
|
||||||
|
})
|
||||||
|
}, 350)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case MINI_APP_MESSAGE.setState: {
|
||||||
|
stateRef.current = { ...stateRef.current, ...(msg.patch as Record<string, unknown>) }
|
||||||
|
postToFrame({ type: MINI_APP_MESSAGE.state, state: stateRef.current })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage)
|
||||||
|
return () => window.removeEventListener('message', handleMessage)
|
||||||
|
}, [app])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
title={app.name}
|
||||||
|
srcDoc={app.html}
|
||||||
|
className="h-full w-full border-0 bg-neutral-950"
|
||||||
|
sandbox={SANDBOX}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
202
apps/x/apps/renderer/src/components/mini-apps-view.tsx
Normal file
202
apps/x/apps/renderer/src/components/mini-apps-view.tsx
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ArrowLeft, Plus } from 'lucide-react'
|
||||||
|
import { MINI_APPS, getMiniApp } from '@/mini-apps/registry'
|
||||||
|
import type { MiniApp } from '@/mini-apps/types'
|
||||||
|
import { MiniAppFrame } from '@/components/mini-app-frame'
|
||||||
|
|
||||||
|
// The "Mini Apps" section: a gallery of premium product tiles; click one to open
|
||||||
|
// the app full-screen. Each card's accent theme and decorative pattern are
|
||||||
|
// derived deterministically from the app id, so identity comes from colour +
|
||||||
|
// typography rather than an icon.
|
||||||
|
|
||||||
|
type Theme = { accent: string; glow: string }
|
||||||
|
|
||||||
|
const THEMES: Theme[] = [
|
||||||
|
{ accent: '#7C5CFF', glow: 'rgba(124,92,255,0.45)' }, // Indigo
|
||||||
|
{ accent: '#4F9CFF', glow: 'rgba(79,156,255,0.45)' }, // Ocean
|
||||||
|
{ accent: '#22C55E', glow: 'rgba(34,197,94,0.40)' }, // Emerald
|
||||||
|
{ accent: '#F59E0B', glow: 'rgba(245,158,11,0.42)' }, // Amber
|
||||||
|
{ accent: '#EC4899', glow: 'rgba(236,72,153,0.42)' }, // Rose
|
||||||
|
{ accent: '#14B8A6', glow: 'rgba(20,184,166,0.40)' }, // Teal
|
||||||
|
]
|
||||||
|
|
||||||
|
const PATTERNS = ['dots', 'grid', 'diagonal', 'radial', 'waves', 'mesh']
|
||||||
|
|
||||||
|
function hash(s: string): number {
|
||||||
|
let h = 0
|
||||||
|
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0
|
||||||
|
return Math.abs(h)
|
||||||
|
}
|
||||||
|
const themeFor = (id: string): Theme => THEMES[hash(id) % THEMES.length]
|
||||||
|
const patternFor = (id: string): string => PATTERNS[hash(id + '·pat') % PATTERNS.length]
|
||||||
|
|
||||||
|
// Card styling lives here (precise gradients/glows/patterns are awkward in
|
||||||
|
// Tailwind tokens). Injected once; per-card accent is passed via CSS variables.
|
||||||
|
const CARD_CSS = `
|
||||||
|
/* Light is the baseline; .dark (set on <html>) overrides the surface tokens.
|
||||||
|
The accent system (badge/pill/glow/pattern via --accent) is identical in both. */
|
||||||
|
.ma-page {
|
||||||
|
--ma-bg:#f6f6f7;
|
||||||
|
--ma-card-from:#ffffff; --ma-card-to:#fbfbfc;
|
||||||
|
--ma-card-hover-from:#ffffff; --ma-card-hover-to:#f5f5f7;
|
||||||
|
--ma-border:rgba(0,0,0,0.08); --ma-border-hover:rgba(0,0,0,0.14);
|
||||||
|
--ma-shadow:0 1px 2px rgba(0,0,0,0.06);
|
||||||
|
--ma-title:#101013; --ma-desc:rgba(0,0,0,0.62);
|
||||||
|
--ma-h1:#101013; --ma-sub:rgba(0,0,0,0.5); --ma-lastrun:rgba(0,0,0,0.42);
|
||||||
|
--ma-off-bg:rgba(0,0,0,0.05); --ma-off-fg:rgba(0,0,0,0.5); --ma-off-dot:rgba(0,0,0,0.4);
|
||||||
|
--ma-new-border:rgba(0,0,0,0.14); --ma-new-title:rgba(0,0,0,0.6); --ma-new-hint:rgba(0,0,0,0.4);
|
||||||
|
--ma-pat-opacity:0.16; --ma-glow-opacity:0.22; --ma-glow-hover-opacity:0.30;
|
||||||
|
--ma-badge-mix:22%; --ma-pill-mix:18%;
|
||||||
|
height:100%; overflow:auto; background:var(--ma-bg);
|
||||||
|
}
|
||||||
|
.dark .ma-page {
|
||||||
|
--ma-bg:#0c0c0e;
|
||||||
|
--ma-card-from:#17171B; --ma-card-to:#111114;
|
||||||
|
--ma-card-hover-from:#19191e; --ma-card-hover-to:#121215;
|
||||||
|
--ma-border:rgba(255,255,255,0.06); --ma-border-hover:rgba(255,255,255,0.09);
|
||||||
|
--ma-shadow:0 1px 2px rgba(0,0,0,0.18);
|
||||||
|
--ma-title:#f5f5f5; --ma-desc:rgba(255,255,255,0.68);
|
||||||
|
--ma-h1:#f5f5f5; --ma-sub:rgba(255,255,255,0.55); --ma-lastrun:rgba(255,255,255,0.4);
|
||||||
|
--ma-off-bg:rgba(255,255,255,0.06); --ma-off-fg:rgba(255,255,255,0.5); --ma-off-dot:rgba(255,255,255,0.45);
|
||||||
|
--ma-new-border:rgba(255,255,255,0.12); --ma-new-title:rgba(255,255,255,0.6); --ma-new-hint:rgba(255,255,255,0.38);
|
||||||
|
--ma-pat-opacity:0.07; --ma-glow-opacity:0.10; --ma-glow-hover-opacity:0.13;
|
||||||
|
--ma-badge-mix:15%; --ma-pill-mix:13%;
|
||||||
|
}
|
||||||
|
.ma-inner { max-width:1080px; margin:0 auto; padding:32px 28px 48px; }
|
||||||
|
.ma-h1 { font-size:24px; font-weight:600; letter-spacing:-0.02em; color:var(--ma-h1); margin:0 0 4px; }
|
||||||
|
.ma-sub { font-size:14px; color:var(--ma-sub); margin:0 0 28px; }
|
||||||
|
.ma-grid { display:grid; grid-template-columns:repeat(3, 1fr); gap:28px; }
|
||||||
|
@media (max-width:1040px) { .ma-grid { grid-template-columns:repeat(2, 1fr); } }
|
||||||
|
@media (max-width:700px) { .ma-grid { grid-template-columns:1fr; } }
|
||||||
|
|
||||||
|
.ma-card {
|
||||||
|
position:relative; min-height:252px; border-radius:20px;
|
||||||
|
border:1px solid var(--ma-border);
|
||||||
|
background:linear-gradient(160deg, var(--ma-card-from) 0%, var(--ma-card-to) 100%);
|
||||||
|
padding:22px; text-align:left; cursor:pointer; overflow:hidden;
|
||||||
|
display:flex; flex-direction:column; isolation:isolate;
|
||||||
|
box-shadow: var(--ma-shadow), 0 6px 18px -20px var(--glow);
|
||||||
|
transition: box-shadow .22s ease, border-color .22s ease, background .22s ease;
|
||||||
|
}
|
||||||
|
.ma-card:hover {
|
||||||
|
border-color: var(--ma-border-hover);
|
||||||
|
background:linear-gradient(160deg, var(--ma-card-hover-from) 0%, var(--ma-card-hover-to) 100%);
|
||||||
|
}
|
||||||
|
/* decorative pattern layer (accent-tinted, very low opacity) */
|
||||||
|
.ma-card::before {
|
||||||
|
content:''; position:absolute; inset:0; z-index:-1; opacity:var(--ma-pat-opacity); pointer-events:none;
|
||||||
|
}
|
||||||
|
/* ambient glow blob, top-right */
|
||||||
|
.ma-card::after {
|
||||||
|
content:''; position:absolute; top:-45%; right:-25%; width:75%; height:75%; z-index:-1;
|
||||||
|
background: radial-gradient(circle, var(--accent) 0%, transparent 70%);
|
||||||
|
opacity:var(--ma-glow-opacity); filter: blur(18px); pointer-events:none; transition: opacity .22s ease;
|
||||||
|
}
|
||||||
|
.ma-card:hover::after { opacity:var(--ma-glow-hover-opacity); }
|
||||||
|
|
||||||
|
.ma-pat-dots::before { background-image: radial-gradient(var(--accent) 1px, transparent 1.4px); background-size:16px 16px; }
|
||||||
|
.ma-pat-grid::before { background-image: linear-gradient(var(--accent) 1px, transparent 1px), linear-gradient(90deg, var(--accent) 1px, transparent 1px); background-size:26px 26px; }
|
||||||
|
.ma-pat-diagonal::before { background-image: repeating-linear-gradient(45deg, var(--accent) 0 1px, transparent 1px 14px); }
|
||||||
|
.ma-pat-radial::before { background-image: radial-gradient(circle at 78% 18%, var(--accent) 0%, transparent 55%); opacity:calc(var(--ma-pat-opacity) + 0.05); }
|
||||||
|
.ma-pat-waves::before { background-image: repeating-radial-gradient(circle at 50% -30%, transparent 0 20px, var(--accent) 20px 21px); }
|
||||||
|
.ma-pat-mesh::before { background-image: radial-gradient(circle at 12% 18%, var(--accent) 0%, transparent 42%), radial-gradient(circle at 88% 82%, var(--accent) 0%, transparent 42%); opacity:calc(var(--ma-pat-opacity) + 0.03); }
|
||||||
|
|
||||||
|
.ma-top { display:flex; justify-content:flex-end; }
|
||||||
|
.ma-badge {
|
||||||
|
display:inline-flex; align-items:center; height:22px; padding:0 10px; border-radius:999px;
|
||||||
|
font-size:9.5px; font-weight:600; letter-spacing:0.07em;
|
||||||
|
color: var(--accent); background: color-mix(in srgb, var(--accent) var(--ma-badge-mix), transparent);
|
||||||
|
}
|
||||||
|
.ma-badge.off { color: var(--ma-off-fg); background: var(--ma-off-bg); }
|
||||||
|
|
||||||
|
.ma-title { font-size:22px; font-weight:600; letter-spacing:-0.02em; color:var(--ma-title); margin:18px 0 8px; }
|
||||||
|
.ma-desc {
|
||||||
|
font-size:15px; font-weight:400; line-height:1.45; color:var(--ma-desc); margin:0;
|
||||||
|
display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
|
||||||
|
}
|
||||||
|
.ma-footer { margin-top:auto; padding-top:22px; display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
||||||
|
.ma-source { font-size:12px; font-weight:600; color:var(--accent); background: color-mix(in srgb, var(--accent) var(--ma-pill-mix), transparent); padding:5px 11px; border-radius:999px; }
|
||||||
|
.ma-lastrun { font-size:12px; color:var(--ma-lastrun); }
|
||||||
|
|
||||||
|
.ma-new {
|
||||||
|
min-height:252px; border-radius:20px; border:1px dashed var(--ma-new-border);
|
||||||
|
background:transparent; display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||||||
|
gap:8px; color:var(--ma-new-title); cursor:default; transition: border-color .2s ease, color .2s ease;
|
||||||
|
}
|
||||||
|
.ma-new-title { font-size:15px; font-weight:600; color:var(--ma-new-title); }
|
||||||
|
.ma-new-hint { font-size:12px; color:var(--ma-new-hint); }
|
||||||
|
`
|
||||||
|
|
||||||
|
function Card({ app, onOpen }: { app: MiniApp; onOpen: () => void }) {
|
||||||
|
const theme = themeFor(app.id)
|
||||||
|
const pattern = patternFor(app.id)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onOpen}
|
||||||
|
className={`ma-card ma-pat-${pattern}`}
|
||||||
|
style={{ '--accent': theme.accent, '--glow': theme.glow } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<div className="ma-top">
|
||||||
|
<span className={`ma-badge${app.active ? '' : ' off'}`}>
|
||||||
|
{app.active ? 'ACTIVE' : 'PAUSED'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="ma-title">{app.name}</div>
|
||||||
|
<div className="ma-desc">{app.description}</div>
|
||||||
|
<div className="ma-footer">
|
||||||
|
<span className="ma-source">{app.source}</span>
|
||||||
|
<span className="ma-lastrun">Last run {app.lastRun}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MiniAppsView() {
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
const selected = selectedId ? getMiniApp(selectedId) : undefined
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
<div className="flex items-center gap-2 border-b border-border px-3 py-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedId(null)}
|
||||||
|
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="size-4" />
|
||||||
|
Apps
|
||||||
|
</button>
|
||||||
|
<span className="text-sm font-medium">{selected.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="min-h-0 flex-1">
|
||||||
|
<MiniAppFrame app={selected} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ma-page">
|
||||||
|
<style>{CARD_CSS}</style>
|
||||||
|
<div className="ma-inner">
|
||||||
|
<h1 className="ma-h1">Mini Apps</h1>
|
||||||
|
<p className="ma-sub">Little apps that live inside Rowboat, powered by your agents and integrations.</p>
|
||||||
|
|
||||||
|
<div className="ma-grid">
|
||||||
|
{MINI_APPS.map((app) => (
|
||||||
|
<Card key={app.id} app={app} onOpen={() => setSelectedId(app.id)} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Placeholder for copilot-generated apps (Phase 3). */}
|
||||||
|
<div className="ma-new">
|
||||||
|
<Plus className="size-5" />
|
||||||
|
<div className="ma-new-title">New app</div>
|
||||||
|
<div className="ma-new-hint">Describe one to the copilot (coming soon)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Globe,
|
Globe,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Home,
|
Home,
|
||||||
|
LayoutGrid,
|
||||||
Mic,
|
Mic,
|
||||||
SquarePen,
|
SquarePen,
|
||||||
Plug,
|
Plug,
|
||||||
|
|
@ -161,6 +162,7 @@ type SidebarContentPanelProps = {
|
||||||
onOpenMeetings?: () => void
|
onOpenMeetings?: () => void
|
||||||
onOpenCode?: () => void
|
onOpenCode?: () => void
|
||||||
onOpenBgTasks?: () => void
|
onOpenBgTasks?: () => void
|
||||||
|
onOpenApps?: () => void
|
||||||
onOpenAgent?: (slug: string) => void
|
onOpenAgent?: (slug: string) => void
|
||||||
recentRuns?: { id: string; title?: string; createdAt: string; modifiedAt?: string }[]
|
recentRuns?: { id: string; title?: string; createdAt: string; modifiedAt?: string }[]
|
||||||
onOpenRun?: (runId: string) => void
|
onOpenRun?: (runId: string) => void
|
||||||
|
|
@ -171,7 +173,7 @@ type SidebarContentPanelProps = {
|
||||||
onToggleBrowser?: () => void
|
onToggleBrowser?: () => void
|
||||||
onVoiceNoteCreated?: (path: string) => void
|
onVoiceNoteCreated?: (path: string) => void
|
||||||
/** Which primary destination is currently active, for nav highlighting. */
|
/** Which primary destination is currently active, for nav highlighting. */
|
||||||
activeNav?: 'home' | 'email' | 'meetings' | 'code' | 'knowledge' | 'agents' | 'workspaces' | null
|
activeNav?: 'home' | 'email' | 'meetings' | 'code' | 'knowledge' | 'agents' | 'apps' | 'workspaces' | null
|
||||||
/** Live meeting recording state, so the recording row can show its indicator/stop. */
|
/** Live meeting recording state, so the recording row can show its indicator/stop. */
|
||||||
meetingRecordingState?: 'idle' | 'connecting' | 'recording' | 'stopping'
|
meetingRecordingState?: 'idle' | 'connecting' | 'recording' | 'stopping'
|
||||||
recordingMeetingSource?: string | null
|
recordingMeetingSource?: string | null
|
||||||
|
|
@ -410,6 +412,7 @@ export function SidebarContentPanel({
|
||||||
onOpenMeetings,
|
onOpenMeetings,
|
||||||
onOpenCode,
|
onOpenCode,
|
||||||
onOpenBgTasks,
|
onOpenBgTasks,
|
||||||
|
onOpenApps,
|
||||||
recentRuns = [],
|
recentRuns = [],
|
||||||
onOpenRun,
|
onOpenRun,
|
||||||
onOpenChatHistory,
|
onOpenChatHistory,
|
||||||
|
|
@ -848,6 +851,15 @@ export function SidebarContentPanel({
|
||||||
</div>
|
</div>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton
|
||||||
|
isActive={activeNav === 'apps'}
|
||||||
|
onClick={onOpenApps}
|
||||||
|
>
|
||||||
|
<LayoutGrid className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="flex-1 truncate text-muted-foreground">Mini Apps</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
isActive={activeNav === 'workspaces'}
|
isActive={activeNav === 'workspaces'}
|
||||||
|
|
|
||||||
46
apps/x/apps/renderer/src/mini-apps/apps/digests.ts
Normal file
46
apps/x/apps/renderer/src/mini-apps/apps/digests.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Two simple digest-style sample Mini Apps (Newsletter Digest, Competitor Watch).
|
||||||
|
// They share the list template in ./simple-list and exist mainly so the gallery
|
||||||
|
// shows the card design across several apps with different accent themes.
|
||||||
|
|
||||||
|
import type { MiniApp } from '../types'
|
||||||
|
import { buildSimpleListHtml } from './simple-list'
|
||||||
|
|
||||||
|
export const newsletterDigestApp: MiniApp = {
|
||||||
|
id: 'newsletter-digest',
|
||||||
|
name: 'Newsletter Digest',
|
||||||
|
description: 'Summarises your subscriptions into a single skimmable morning read.',
|
||||||
|
source: 'Email',
|
||||||
|
active: true,
|
||||||
|
lastRun: '1h ago',
|
||||||
|
scope: ['gmail'],
|
||||||
|
data: {
|
||||||
|
title: 'Newsletter Digest',
|
||||||
|
subtitle: 'Your subscriptions, summarised — 6 this morning',
|
||||||
|
items: [
|
||||||
|
{ id: 'n1', title: 'Stratechery', meta: '7 min', body: 'The platform shift to agents mirrors the mobile transition — distribution, not models, decides the winners.' },
|
||||||
|
{ id: 'n2', title: 'Lenny’s Newsletter', meta: '5 min', body: 'How three PMs run discovery without a researcher: weekly user calls, a shared notes doc, and ruthless prioritisation.' },
|
||||||
|
{ id: 'n3', title: 'The Pragmatic Engineer', meta: '9 min', body: 'Inside a staff-level promo packet: scope, impact, and the "without you it wouldn’t have happened" test.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
html: buildSimpleListHtml('Newsletter Digest'),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const competitorWatchApp: MiniApp = {
|
||||||
|
id: 'competitor-watch',
|
||||||
|
name: 'Competitor Watch',
|
||||||
|
description: 'Tracks launches, pricing changes, and notable posts from your rivals.',
|
||||||
|
source: 'Web',
|
||||||
|
active: false,
|
||||||
|
lastRun: '4h ago',
|
||||||
|
scope: ['web'],
|
||||||
|
data: {
|
||||||
|
title: 'Competitor Watch',
|
||||||
|
subtitle: '3 updates since yesterday',
|
||||||
|
items: [
|
||||||
|
{ id: 'c1', title: 'Acme launched AI Mode', meta: 'pricing', body: 'New $40/mo tier bundles their assistant; older Pro plan unchanged. Positioning leans on “team workspaces”.' },
|
||||||
|
{ id: 'c2', title: 'Globex changelog', meta: 'product', body: 'Shipped offline sync and a public API. Docs hint at a desktop app in private beta.' },
|
||||||
|
{ id: 'c3', title: 'Initech blog', meta: 'content', body: 'A “build vs buy” post aimed squarely at your ICP — worth a counter-piece on local-first privacy.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
html: buildSimpleListHtml('Competitor Watch'),
|
||||||
|
}
|
||||||
43
apps/x/apps/renderer/src/mini-apps/apps/simple-list.ts
Normal file
43
apps/x/apps/renderer/src/mini-apps/apps/simple-list.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Lightweight Mini App template: a clean single-column list of items.
|
||||||
|
//
|
||||||
|
// Used for simple "digest"-style apps (newsletter summaries, competitor updates)
|
||||||
|
// so the gallery shows the card design across multiple apps. Same dependency-free
|
||||||
|
// vanilla approach as the Twitter client.
|
||||||
|
|
||||||
|
import { buildMiniAppHtml } from '../runtime'
|
||||||
|
|
||||||
|
export type ListItem = { id: string; title: string; meta: string; body: string }
|
||||||
|
|
||||||
|
const style = `
|
||||||
|
.app { height:100%; overflow:auto; background:#0a0a0b; color:#e7e9ea; }
|
||||||
|
.wrap { max-width:640px; margin:0 auto; padding:24px 16px; }
|
||||||
|
.h { font-size:22px; font-weight:600; letter-spacing:-0.02em; margin:0 0 4px; color:#f5f5f5; }
|
||||||
|
.sub { font-size:13px; color:#71767b; margin:0 0 20px; }
|
||||||
|
.item { border:1px solid rgba(255,255,255,0.07); background:#141417; border-radius:14px; padding:16px; margin-bottom:12px; }
|
||||||
|
.it-top { display:flex; justify-content:space-between; align-items:baseline; gap:12px; }
|
||||||
|
.it-title { font-size:15px; font-weight:600; }
|
||||||
|
.it-meta { font-size:12px; color:#71767b; flex:0 0 auto; }
|
||||||
|
.it-body { font-size:14px; line-height:1.5; color:rgba(255,255,255,0.7); margin:6px 0 0; }
|
||||||
|
.loading { padding:48px 16px; text-align:center; color:#71767b; }
|
||||||
|
`
|
||||||
|
|
||||||
|
const script = `
|
||||||
|
var root = document.getElementById('root');
|
||||||
|
var current = null;
|
||||||
|
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||||
|
function render(){
|
||||||
|
if(!current){ root.innerHTML = '<div class="app"><div class="loading">Loading…</div></div>'; return; }
|
||||||
|
var items = (current.items||[]).map(function(it){
|
||||||
|
return '<div class="item"><div class="it-top"><div class="it-title">'+esc(it.title)+'</div><div class="it-meta">'+esc(it.meta)+'</div></div>'
|
||||||
|
+ '<p class="it-body">'+esc(it.body)+'</p></div>';
|
||||||
|
}).join('');
|
||||||
|
root.innerHTML = '<div class="app"><div class="wrap"><h1 class="h">'+esc(current.title)+'</h1><p class="sub">'+esc(current.subtitle)+'</p>'+items+'</div></div>';
|
||||||
|
}
|
||||||
|
window.rowboat.onData(function(d){ current = d; render(); });
|
||||||
|
render();
|
||||||
|
window.rowboat.ready();
|
||||||
|
`
|
||||||
|
|
||||||
|
export function buildSimpleListHtml(title: string): string {
|
||||||
|
return buildMiniAppHtml({ title, style, script })
|
||||||
|
}
|
||||||
236
apps/x/apps/renderer/src/mini-apps/apps/twitter-client.ts
Normal file
236
apps/x/apps/renderer/src/mini-apps/apps/twitter-client.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
// Sample Mini App: a Twitter/X client.
|
||||||
|
//
|
||||||
|
// An X-style single-column timeline of important posts the agent has curated,
|
||||||
|
// with simple topic chips to filter what you see. Phase 1 ships static `data`
|
||||||
|
// and stubbed actions; later the bg-tasks engine produces the feed on a trigger
|
||||||
|
// and the bridge runs real Composio actions.
|
||||||
|
//
|
||||||
|
// Dependency-free vanilla JS + CSS so it renders reliably in the sandboxed
|
||||||
|
// iframe with no network. window.rowboat is the only channel to the host;
|
||||||
|
// selected topics persist via rowboat.setState.
|
||||||
|
|
||||||
|
import { buildMiniAppHtml } from '../runtime'
|
||||||
|
import type { MiniApp } from '../types'
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
handle: '@you',
|
||||||
|
topics: ['AI', 'Startups', 'Dev', 'Design'],
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
id: 't1',
|
||||||
|
author: 'Andrej Karpathy',
|
||||||
|
handle: '@karpathy',
|
||||||
|
avatar: '🧠',
|
||||||
|
time: '2h',
|
||||||
|
topics: ['AI', 'Dev'],
|
||||||
|
text: 'The hottest new programming language is English. Spend your time crafting the prompt, not the syntax.',
|
||||||
|
likes: 1240,
|
||||||
|
reposts: 312,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't2',
|
||||||
|
author: 'Vercel',
|
||||||
|
handle: '@vercel',
|
||||||
|
avatar: '▲',
|
||||||
|
time: '5h',
|
||||||
|
topics: ['Dev'],
|
||||||
|
text: 'Shipping is a feature. We just cut cold starts by another 40% on the edge runtime.',
|
||||||
|
likes: 842,
|
||||||
|
reposts: 96,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't3',
|
||||||
|
author: 'Indie Hackers',
|
||||||
|
handle: '@IndieHackers',
|
||||||
|
avatar: '🚀',
|
||||||
|
time: '1h',
|
||||||
|
topics: ['Startups'],
|
||||||
|
text: 'You do not need 1,000 true fans. You need 100 who will tell 10 friends each. Build for the tellers.',
|
||||||
|
likes: 503,
|
||||||
|
reposts: 121,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't4',
|
||||||
|
author: 'Sarah Dev',
|
||||||
|
handle: '@sarah_builds',
|
||||||
|
avatar: '👩💻',
|
||||||
|
time: '32m',
|
||||||
|
topics: ['AI', 'Startups'],
|
||||||
|
text: 'Anyone using local-first AI desktop apps day to day? Curious what actually sticks vs. demo-ware.',
|
||||||
|
likes: 88,
|
||||||
|
reposts: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't5',
|
||||||
|
author: 'Design Notes',
|
||||||
|
handle: '@designnotes',
|
||||||
|
avatar: '🎨',
|
||||||
|
time: '3h',
|
||||||
|
topics: ['Design'],
|
||||||
|
text: 'Good defaults beat good settings. Every preference you add is a decision you forced on the user.',
|
||||||
|
likes: 967,
|
||||||
|
reposts: 204,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't6',
|
||||||
|
author: 'Founder Diary',
|
||||||
|
handle: '@founderdiary',
|
||||||
|
avatar: '📈',
|
||||||
|
time: '6h',
|
||||||
|
topics: ['Startups', 'Design'],
|
||||||
|
text: 'Talked to 12 users this week. Shipped 0 features. Best week in a month.',
|
||||||
|
likes: 1502,
|
||||||
|
reposts: 388,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = `
|
||||||
|
.app { height:100%; overflow:auto; background:#000; color:#e7e9ea; }
|
||||||
|
.feed { max-width:600px; margin:0 auto; border-left:1px solid #2f3336; border-right:1px solid #2f3336; min-height:100%; }
|
||||||
|
.header { position:sticky; top:0; backdrop-filter:blur(12px); background:rgba(0,0,0,.65); border-bottom:1px solid #2f3336; padding:12px 16px; z-index:2; }
|
||||||
|
.h-title { font-size:20px; font-weight:800; margin:0 0 10px; }
|
||||||
|
.chips { display:flex; gap:8px; flex-wrap:wrap; }
|
||||||
|
.chip { border:1px solid #536471; background:transparent; color:#e7e9ea; border-radius:999px; padding:5px 14px; font-size:13px; font-weight:600; cursor:pointer; }
|
||||||
|
.chip.on { background:#1d9bf0; border-color:#1d9bf0; color:#fff; }
|
||||||
|
.post { display:flex; gap:12px; padding:12px 16px; border-bottom:1px solid #2f3336; }
|
||||||
|
.post:hover { background:#080808; }
|
||||||
|
.avatar { width:40px; height:40px; border-radius:50%; background:#16181c; display:flex; align-items:center; justify-content:center; font-size:18px; flex:0 0 auto; }
|
||||||
|
.pbody { flex:1; min-width:0; }
|
||||||
|
.line { display:flex; align-items:center; gap:4px; font-size:15px; }
|
||||||
|
.name { font-weight:700; }
|
||||||
|
.handle, .dot, .time { color:#71767b; font-weight:400; }
|
||||||
|
.text { font-size:15px; line-height:1.4; margin:2px 0 0; white-space:pre-wrap; word-wrap:break-word; }
|
||||||
|
.tags { margin-top:6px; display:flex; gap:8px; flex-wrap:wrap; }
|
||||||
|
.tag { font-size:12px; color:#1d9bf0; }
|
||||||
|
.actions { display:flex; justify-content:space-between; max-width:300px; margin-top:8px; }
|
||||||
|
.action { display:flex; align-items:center; gap:6px; background:none; border:0; color:#71767b; font-size:13px; cursor:pointer; padding:4px; border-radius:999px; }
|
||||||
|
.action:hover { color:#1d9bf0; }
|
||||||
|
.action.like.on { color:#f91880; }
|
||||||
|
.action.repost.on { color:#00ba7c; }
|
||||||
|
.empty { padding:48px 16px; text-align:center; color:#71767b; font-size:15px; }
|
||||||
|
.toast { position:fixed; bottom:20px; left:50%; transform:translateX(-50%); background:#1d9bf0; color:#fff; font-size:14px; font-weight:600; padding:10px 18px; border-radius:999px; opacity:0; transition:opacity .2s; pointer-events:none; }
|
||||||
|
.toast.show { opacity:1; }
|
||||||
|
.loading { padding:48px 16px; text-align:center; color:#71767b; font-size:15px; }
|
||||||
|
`
|
||||||
|
|
||||||
|
// Vanilla JS. No backticks (so it embeds cleanly); window.rowboat is the bridge.
|
||||||
|
const script = `
|
||||||
|
var root = document.getElementById('root');
|
||||||
|
var current = null;
|
||||||
|
var selected = null; // selected topic names
|
||||||
|
var liked = {}; // id -> bool
|
||||||
|
var reposted = {}; // id -> bool
|
||||||
|
var toastTimer = null;
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
function fmt(n) {
|
||||||
|
return n >= 1000 ? (n / 1000).toFixed(1).replace('.0', '') + 'K' : String(n);
|
||||||
|
}
|
||||||
|
function persist() {
|
||||||
|
window.rowboat.setState({ topics: selected, liked: liked, reposted: reposted });
|
||||||
|
}
|
||||||
|
|
||||||
|
function postHtml(p) {
|
||||||
|
var likeOn = liked[p.id] ? ' on' : '';
|
||||||
|
var repoOn = reposted[p.id] ? ' on' : '';
|
||||||
|
var likeCt = p.likes + (liked[p.id] ? 1 : 0);
|
||||||
|
var repoCt = p.reposts + (reposted[p.id] ? 1 : 0);
|
||||||
|
var tags = (p.topics || []).map(function (t) { return '<span class="tag">#' + esc(t) + '</span>'; }).join('');
|
||||||
|
return '<div class="post">'
|
||||||
|
+ '<div class="avatar">' + esc(p.avatar) + '</div>'
|
||||||
|
+ '<div class="pbody">'
|
||||||
|
+ '<div class="line"><span class="name">' + esc(p.author) + '</span>'
|
||||||
|
+ '<span class="handle">' + esc(p.handle) + '</span><span class="dot">·</span><span class="time">' + esc(p.time) + '</span></div>'
|
||||||
|
+ '<p class="text">' + esc(p.text) + '</p>'
|
||||||
|
+ (tags ? '<div class="tags">' + tags + '</div>' : '')
|
||||||
|
+ '<div class="actions">'
|
||||||
|
+ '<button class="action reply" data-action="reply" data-id="' + p.id + '">💬</button>'
|
||||||
|
+ '<button class="action repost' + repoOn + '" data-action="repost" data-id="' + p.id + '">🔁 <span class="ct">' + fmt(repoCt) + '</span></button>'
|
||||||
|
+ '<button class="action like' + likeOn + '" data-action="like" data-id="' + p.id + '">' + (liked[p.id] ? '♥' : '♡') + ' <span class="ct">' + fmt(likeCt) + '</span></button>'
|
||||||
|
+ '</div></div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!current) { root.innerHTML = '<div class="app"><div class="feed"><div class="loading">Loading your feed…</div></div></div>'; return; }
|
||||||
|
var chips = current.topics.map(function (t) {
|
||||||
|
var on = selected.indexOf(t) >= 0 ? ' on' : '';
|
||||||
|
return '<button class="chip' + on + '" data-topic="' + esc(t) + '">' + esc(t) + '</button>';
|
||||||
|
}).join('');
|
||||||
|
var visible = current.posts.filter(function (p) {
|
||||||
|
return (p.topics || []).some(function (t) { return selected.indexOf(t) >= 0; });
|
||||||
|
});
|
||||||
|
var body = visible.length
|
||||||
|
? visible.map(postHtml).join('')
|
||||||
|
: '<div class="empty">No posts. Pick a topic above to see what is happening.</div>';
|
||||||
|
root.innerHTML = '<div class="app"><div class="feed">'
|
||||||
|
+ '<div class="header"><h1 class="h-title">Home</h1><div class="chips">' + chips + '</div></div>'
|
||||||
|
+ body
|
||||||
|
+ '</div><div class="toast" id="toast"></div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function flash(msg) {
|
||||||
|
var el = document.getElementById('toast');
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = msg;
|
||||||
|
el.className = 'toast show';
|
||||||
|
if (toastTimer) clearTimeout(toastTimer);
|
||||||
|
toastTimer = setTimeout(function () { el.className = 'toast'; }, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.addEventListener('click', function (e) {
|
||||||
|
var t = e.target;
|
||||||
|
var chip = t && t.closest ? t.closest('.chip') : null;
|
||||||
|
if (chip) {
|
||||||
|
var topic = chip.getAttribute('data-topic');
|
||||||
|
var i = selected.indexOf(topic);
|
||||||
|
if (i >= 0) selected.splice(i, 1); else selected.push(topic);
|
||||||
|
persist();
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var btn = t && t.closest ? t.closest('.action') : null;
|
||||||
|
if (!btn) return;
|
||||||
|
var action = btn.getAttribute('data-action');
|
||||||
|
var id = btn.getAttribute('data-id');
|
||||||
|
if (action === 'reply') { flash('Reply drafted (demo)'); return; }
|
||||||
|
if (action === 'like') { liked[id] = !liked[id]; }
|
||||||
|
if (action === 'repost') { reposted[id] = !reposted[id]; }
|
||||||
|
persist();
|
||||||
|
render();
|
||||||
|
window.rowboat.callAction('twitter', action, { id: id }).then(function (res) {
|
||||||
|
flash((res && res.message) || 'Done');
|
||||||
|
}).catch(function (err) {
|
||||||
|
flash('Failed: ' + (err && err.message ? err.message : err));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.rowboat.onData(function (d) {
|
||||||
|
current = d;
|
||||||
|
if (selected === null) selected = d.topics.slice();
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
window.rowboat.onState(function (st) {
|
||||||
|
if (!st) return;
|
||||||
|
if (st.topics) selected = st.topics;
|
||||||
|
if (st.liked) liked = st.liked;
|
||||||
|
if (st.reposted) reposted = st.reposted;
|
||||||
|
if (current) render();
|
||||||
|
});
|
||||||
|
render();
|
||||||
|
window.rowboat.ready();
|
||||||
|
`
|
||||||
|
|
||||||
|
export const twitterClientApp: MiniApp = {
|
||||||
|
id: 'twitter-client',
|
||||||
|
name: 'Twitter, curated',
|
||||||
|
description: 'An X-style feed of the posts that matter, filtered by your topics.',
|
||||||
|
source: 'Twitter',
|
||||||
|
active: true,
|
||||||
|
lastRun: '2m ago',
|
||||||
|
scope: ['twitter'],
|
||||||
|
data,
|
||||||
|
html: buildMiniAppHtml({ title: 'Twitter, curated', style, script }),
|
||||||
|
}
|
||||||
14
apps/x/apps/renderer/src/mini-apps/registry.ts
Normal file
14
apps/x/apps/renderer/src/mini-apps/registry.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Mini Apps registry.
|
||||||
|
//
|
||||||
|
// Phase 1: apps are hardcoded here. Later phases replace this with apps loaded
|
||||||
|
// from ~/.rowboat/apps/<id>/ over IPC.
|
||||||
|
|
||||||
|
import type { MiniApp } from './types'
|
||||||
|
import { twitterClientApp } from './apps/twitter-client'
|
||||||
|
import { newsletterDigestApp, competitorWatchApp } from './apps/digests'
|
||||||
|
|
||||||
|
export const MINI_APPS: MiniApp[] = [twitterClientApp, newsletterDigestApp, competitorWatchApp]
|
||||||
|
|
||||||
|
export function getMiniApp(id: string): MiniApp | undefined {
|
||||||
|
return MINI_APPS.find((app) => app.id === id)
|
||||||
|
}
|
||||||
108
apps/x/apps/renderer/src/mini-apps/runtime.ts
Normal file
108
apps/x/apps/renderer/src/mini-apps/runtime.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
// Mini Apps — app HTML scaffolding shared by every app.
|
||||||
|
//
|
||||||
|
// `buildMiniAppHtml` wraps an app's markup/JS in a single self-contained HTML
|
||||||
|
// document and injects the `window.rowboat` bridge shim before the app runs.
|
||||||
|
//
|
||||||
|
// Phase 1 is deliberately dependency-free: NO remote CDNs and NO in-browser
|
||||||
|
// transpile. Apps are plain HTML + CSS + JS so they render reliably inside the
|
||||||
|
// sandboxed, opaque-origin iframe and work offline. Later phases can layer in a
|
||||||
|
// locally-bundled React runtime (esbuild-at-save) without changing the bridge.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bridge shim, injected as a plain <script> before the app's script. It
|
||||||
|
* defines `window.rowboat` and speaks the postMessage protocol in ./types.ts.
|
||||||
|
* This is the only channel the app has to the host.
|
||||||
|
*/
|
||||||
|
const BRIDGE_SHIM = /* js */ `
|
||||||
|
(function () {
|
||||||
|
var data = null, state = null;
|
||||||
|
var dataCbs = [], stateCbs = [];
|
||||||
|
var pending = {}, seq = 0;
|
||||||
|
function post(msg) { parent.postMessage(msg, '*'); }
|
||||||
|
window.addEventListener('message', function (e) {
|
||||||
|
var m = e.data;
|
||||||
|
if (!m || typeof m !== 'object') return;
|
||||||
|
if (m.type === 'rowboat:mini-app:data') {
|
||||||
|
data = m.data;
|
||||||
|
dataCbs.forEach(function (cb) { try { cb(data); } catch (_) {} });
|
||||||
|
} else if (m.type === 'rowboat:mini-app:state') {
|
||||||
|
state = m.state;
|
||||||
|
stateCbs.forEach(function (cb) { try { cb(state); } catch (_) {} });
|
||||||
|
} else if (m.type === 'rowboat:mini-app:action-result') {
|
||||||
|
var p = pending[m.id];
|
||||||
|
if (p) {
|
||||||
|
delete pending[m.id];
|
||||||
|
if (m.ok) p.resolve(m.result); else p.reject(new Error(m.error || 'action failed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.rowboat = {
|
||||||
|
getData: function () { return data; },
|
||||||
|
onData: function (cb) {
|
||||||
|
dataCbs.push(cb);
|
||||||
|
if (data !== null) { try { cb(data); } catch (_) {} }
|
||||||
|
return function () { var i = dataCbs.indexOf(cb); if (i >= 0) dataCbs.splice(i, 1); };
|
||||||
|
},
|
||||||
|
getState: function () { return state; },
|
||||||
|
onState: function (cb) {
|
||||||
|
stateCbs.push(cb);
|
||||||
|
if (state !== null) { try { cb(state); } catch (_) {} }
|
||||||
|
return function () { var i = stateCbs.indexOf(cb); if (i >= 0) stateCbs.splice(i, 1); };
|
||||||
|
},
|
||||||
|
setState: function (patch) {
|
||||||
|
state = Object.assign({}, state || {}, patch);
|
||||||
|
post({ type: 'rowboat:mini-app:setState', patch: patch });
|
||||||
|
stateCbs.forEach(function (cb) { try { cb(state); } catch (_) {} });
|
||||||
|
},
|
||||||
|
callAction: function (scope, action, args) {
|
||||||
|
var id = 'a' + (++seq);
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
pending[id] = { resolve: resolve, reject: reject };
|
||||||
|
post({ type: 'rowboat:mini-app:action', id: id, scope: scope, action: action, args: args });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ready: function () { post({ type: 'rowboat:mini-app:ready' }); },
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a complete self-contained HTML document for a Mini App.
|
||||||
|
* @param title Document title.
|
||||||
|
* @param style App CSS (inlined into a <style> tag).
|
||||||
|
* @param body Initial body markup (often just a mount node).
|
||||||
|
* @param script App JS. Runs after the bridge shim; `window.rowboat` is ready.
|
||||||
|
*/
|
||||||
|
export function buildMiniAppHtml({
|
||||||
|
title,
|
||||||
|
style = '',
|
||||||
|
body = '<div id="root"></div>',
|
||||||
|
script,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
style?: string
|
||||||
|
body?: string
|
||||||
|
script: string
|
||||||
|
}): string {
|
||||||
|
return `<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>${title}</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
html, body { height: 100%; margin: 0; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
||||||
|
${style}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${body}
|
||||||
|
<script>${BRIDGE_SHIM}</script>
|
||||||
|
<script>
|
||||||
|
${script}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
}
|
||||||
74
apps/x/apps/renderer/src/mini-apps/types.ts
Normal file
74
apps/x/apps/renderer/src/mini-apps/types.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Mini Apps — shared types and the host <-> iframe bridge protocol.
|
||||||
|
//
|
||||||
|
// Phase 1 is UI-only: apps are hardcoded in the renderer (see registry.ts) and
|
||||||
|
// rendered in a sandboxed iframe (see components/mini-app-frame.tsx). The shapes
|
||||||
|
// here intentionally mirror the eventual on-disk model (one self-contained folder
|
||||||
|
// per app under ~/.rowboat/apps/<id>/) so later phases can slot in without a
|
||||||
|
// rewrite.
|
||||||
|
|
||||||
|
/** A single Mini App. */
|
||||||
|
export type MiniApp = {
|
||||||
|
/** Stable slug; also the eventual on-disk folder name. The card's accent
|
||||||
|
* theme and decorative pattern are derived deterministically from this. */
|
||||||
|
id: string
|
||||||
|
/** Display name shown on the card and in the open view. */
|
||||||
|
name: string
|
||||||
|
/** One-line description for the card (clamped to 2 lines). */
|
||||||
|
description: string
|
||||||
|
/** Primary integration shown in the card footer pill (e.g. 'Twitter'). */
|
||||||
|
source: string
|
||||||
|
/** Whether the app's agent is currently active (drives the status badge). */
|
||||||
|
active: boolean
|
||||||
|
/** Human last-run label for the card footer (e.g. '2m ago'). Static in V1. */
|
||||||
|
lastRun: string
|
||||||
|
/**
|
||||||
|
* Composio integration scope this app is allowed to touch. Drives bridge
|
||||||
|
* enforcement and the auth prompt in later phases; informational in Phase 1.
|
||||||
|
*/
|
||||||
|
scope: string[]
|
||||||
|
/**
|
||||||
|
* The app's frontend: a single self-contained HTML document (React + Tailwind
|
||||||
|
* + Babel via CDN). Rendered via the iframe `srcdoc` attribute.
|
||||||
|
*/
|
||||||
|
html: string
|
||||||
|
/**
|
||||||
|
* The latest agent "backend" output. Static in Phase 1; produced by the agent
|
||||||
|
* on a trigger in later phases. Delivered to the iframe via the bridge.
|
||||||
|
*/
|
||||||
|
data: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Bridge protocol (host <-> iframe via postMessage).
|
||||||
|
//
|
||||||
|
// The app code inside the iframe talks to a small `window.rowboat` shim (injected
|
||||||
|
// as part of the app HTML) which speaks these messages. This is both the product
|
||||||
|
// surface the app codes against and the security boundary.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Messages sent from the iframe (app) up to the host (renderer). */
|
||||||
|
export type MiniAppOutboundMessage =
|
||||||
|
/** Handshake: app is mounted and wants its initial data + state. */
|
||||||
|
| { type: 'rowboat:mini-app:ready' }
|
||||||
|
/** App requests a scoped Composio action. Host replies with action-result. */
|
||||||
|
| { type: 'rowboat:mini-app:action'; id: string; scope: string; action: string; args?: unknown }
|
||||||
|
/** App wants to persist a patch to its per-app state store. */
|
||||||
|
| { type: 'rowboat:mini-app:setState'; patch: unknown }
|
||||||
|
|
||||||
|
/** Messages sent from the host (renderer) down to the iframe (app). */
|
||||||
|
export type MiniAppInboundMessage =
|
||||||
|
/** Latest agent data; sent on ready and whenever data refreshes. */
|
||||||
|
| { type: 'rowboat:mini-app:data'; data: unknown }
|
||||||
|
/** Current per-app state; sent on ready and after setState. */
|
||||||
|
| { type: 'rowboat:mini-app:state'; state: unknown }
|
||||||
|
/** Result of a previously requested action, correlated by id. */
|
||||||
|
| { type: 'rowboat:mini-app:action-result'; id: string; ok: boolean; result?: unknown; error?: string }
|
||||||
|
|
||||||
|
export const MINI_APP_MESSAGE = {
|
||||||
|
ready: 'rowboat:mini-app:ready',
|
||||||
|
action: 'rowboat:mini-app:action',
|
||||||
|
setState: 'rowboat:mini-app:setState',
|
||||||
|
data: 'rowboat:mini-app:data',
|
||||||
|
state: 'rowboat:mini-app:state',
|
||||||
|
actionResult: 'rowboat:mini-app:action-result',
|
||||||
|
} as const
|
||||||
Loading…
Add table
Add a link
Reference in a new issue