-Rowboat connects your email and meeting notes, builds long-lived knowledge from them, and uses that knowledge to help get work done on your machine.
+Rowboat connects to your email and meeting notes, builds a long-lived knowledge graph, and uses that context to help you get work done - privately, on your machine.
+
+You can do things like:
+- `Build me a deck about our next quarter roadmap` → generates a PDF using context from your knowledge graph
+- `Prep me for my meeting with Alex` → pulls past decisions, open questions, and relevant threads into a crisp brief (or a voice note)
+- Visualize, edit, and update your knowledge graph anytime (it’s just Markdown)
+- Record voice memos that automatically capture and update key takeaways in the graph
+
+Download latest for Mac/Windows/Linux: [Download](https://www.rowboatlabs.com/)
----
## Demo
-[](https://www.youtube.com/watch?v=T2Bmiy05FrI)
+
+[](https://www.youtube.com/watch?v=5AWoGo-L16I)
+
+[Watch the full video](https://www.youtube.com/watch?v=5AWoGo-L16I)
---
-## Quick start
+## Installation
-**Download for Mac:**
+**Download latest for Mac/Windows/Linux:** [Download](https://www.rowboatlabs.com/)
-https://github.com/rowboatlabs/rowboat/releases/latest
+**All release files:** https://github.com/rowboatlabs/rowboat/releases/latest
## What it does
-Rowboat ingests your:
-- **Email** (Gmail)
-- **Meeting notes** (Granola, Fireflies)
+Rowboat is a **local-first AI coworker** that can:
+- **Remember** the important context you don’t want to re-explain (people, projects, decisions, commitments)
+- **Understand** what’s relevant right now (before a meeting, while replying to an email, when writing a doc)
+- **Help you act** by drafting, summarizing, planning, and producing real artifacts (briefs, emails, docs, PDF slides)
-and organizes them into a local, Obsidian-compatible vault of plain Markdown files with backlinks.
+Under the hood, Rowboat maintains an **Obsidian-compatible vault** of plain Markdown notes with backlinks — a transparent “working memory” you can inspect and edit.
-This vault is not just for browsing or search. It becomes a working memory that Rowboat’s AI uses to take actions on your behalf.
+## Integrations
-As new emails and meetings come in, the relevant notes update automatically, building persistent context across people, projects, organizations, and topics.
-
----
+Rowboat builds memory from the work you already do, including:
+- **Gmail** (email)
+- **Granola** (meeting notes)
+- **Fireflies** (meeting notes)
## How it’s different
@@ -70,33 +82,53 @@ Rowboat maintains **long-lived knowledge** instead:
The result is memory that compounds, rather than retrieval that starts cold every time.
----
-
## What you can do with it
-Rowboat uses this knowledge to help with everyday work, including:
+- **Meeting prep** from prior decisions, threads, and open questions
+- **Email drafting** grounded in history and commitments
+- **Docs & decks** generated from your ongoing context (including PDF slides)
+- **Follow-ups**: capture decisions, action items, and owners so nothing gets dropped
+- **On-your-machine help**: create files, summarize into notes, and run workflows using local tools (with explicit, reviewable actions)
-- Drafting emails using accumulated context
-- Preparing for meetings from prior decisions and discussions
-- Organizing files and project artifacts as work evolves
-- Running shell commands or scripts as agent actions
-- Extending capabilities via external tools and MCP servers
+## Background agents
-Actions are explicit and grounded in the current state of your knowledge.
+Rowboat can spin up **background agents** to do repeatable work automatically - so routine tasks happen without you having to ask every time.
----
+Examples:
+- Draft email replies in the background (grounded in your past context and commitments)
+- Generate a daily voice note each morning (agenda, priorities, upcoming meetings)
+- Create recurring project updates from the latest emails/notes
+- Keep your knowledge graph up to date as new information comes in
+
+You control what runs, when it runs, and what gets written back into your local Markdown vault.
+
+## Bring your own model
+
+Rowboat works with the model setup you prefer:
+- **Local models** via Ollama or LM Studio
+- **Hosted models** (bring your own API key/provider)
+- Swap models anytime — your data stays in your local Markdown vault
+
+## Extend Rowboat with tools (MCP)
+
+Rowboat can connect to external tools and services via **Model Context Protocol (MCP)**.
+That means you can plug in (for example) search, databases, CRMs, support tools, and automations - or your own internal tools.
+
+Examples: Exa (web search), Twitter/X, ElevenLabs (voice), Slack, Linear/Jira, GitHub, and more.
## Local-first by design
- All data is stored locally as plain Markdown
- No proprietary formats or hosted lock-in
-- Works with local models via Ollama or LM Studio, or hosted models if you prefer
- You can inspect, edit, back up, or delete everything at any time
+
+## Looking for Rowboat Web Studio?
+
+If you’re looking for Rowboat web Studio, start [here](https://docs.rowboatlabs.com/).
+
---
-
-Made with ❤️ by the Rowboat team
[Discord](https://discord.com/invite/htdKpBZF) · [Twitter](https://x.com/intent/user?screen_name=rowboatlabshq)
diff --git a/apps/docs/docs/img/google-setup/01-select-project-dropdown.png b/apps/docs/docs/img/google-setup/01-select-project-dropdown.png
new file mode 100644
index 00000000..ed73ee5a
Binary files /dev/null and b/apps/docs/docs/img/google-setup/01-select-project-dropdown.png differ
diff --git a/apps/docs/docs/img/google-setup/02-enable-api.png b/apps/docs/docs/img/google-setup/02-enable-api.png
new file mode 100644
index 00000000..fd154f7e
Binary files /dev/null and b/apps/docs/docs/img/google-setup/02-enable-api.png differ
diff --git a/apps/docs/docs/img/google-setup/03-oauth-consent-screen.png b/apps/docs/docs/img/google-setup/03-oauth-consent-screen.png
new file mode 100644
index 00000000..25d6dadc
Binary files /dev/null and b/apps/docs/docs/img/google-setup/03-oauth-consent-screen.png differ
diff --git a/apps/docs/docs/img/google-setup/04-add-test-users.png b/apps/docs/docs/img/google-setup/04-add-test-users.png
new file mode 100644
index 00000000..774ea9e9
Binary files /dev/null and b/apps/docs/docs/img/google-setup/04-add-test-users.png differ
diff --git a/apps/docs/docs/img/google-setup/05-create-oauth-client-uwp.png b/apps/docs/docs/img/google-setup/05-create-oauth-client-uwp.png
new file mode 100644
index 00000000..eaa73755
Binary files /dev/null and b/apps/docs/docs/img/google-setup/05-create-oauth-client-uwp.png differ
diff --git a/apps/docs/docs/img/google-setup/06-copy-client-id.png b/apps/docs/docs/img/google-setup/06-copy-client-id.png
new file mode 100644
index 00000000..ea0df758
Binary files /dev/null and b/apps/docs/docs/img/google-setup/06-copy-client-id.png differ
diff --git a/apps/x/apps/main/forge.config.cjs b/apps/x/apps/main/forge.config.cjs
index 3a2b340f..57f733f2 100644
--- a/apps/x/apps/main/forge.config.cjs
+++ b/apps/x/apps/main/forge.config.cjs
@@ -3,10 +3,10 @@
// Forge loads configs with require(), which fails on ESM files
const path = require('path');
+const pkg = require('./package.json');
module.exports = {
packagerConfig: {
- name: 'Rowboat',
executableName: 'rowboat',
icon: './icons/icon', // .icns extension added automatically
appBundleId: 'com.rowboat.app',
@@ -19,9 +19,6 @@ module.exports = {
appleIdPassword: process.env.APPLE_PASSWORD,
teamId: process.env.APPLE_TEAM_ID
},
- // NOTE: Electron Forge ignores packagerConfig.dir and always packages from the
- // config file's directory. We use packageAfterCopy hook instead to customize output.
- // dir: path.join(__dirname, '.package'), // Not supported by Forge
// Since we bundle everything with esbuild, we don't need node_modules at all.
// These settings prevent Forge's dependency walker (flora-colossus) from trying
// to analyze/copy node_modules, which fails with pnpm's symlinked workspaces.
@@ -39,27 +36,55 @@ module.exports = {
name: '@electron-forge/maker-dmg',
config: (arch) => ({
format: 'ULFO',
- name: `Rowboat-${arch}`, // Architecture-specific name to avoid conflicts
+ name: `Rowboat-darwin-${arch}-${pkg.version}`, // Architecture-specific name to avoid conflicts
})
},
{
- name: '@electron-forge/maker-zip',
- platforms: ['darwin'],
- // ZIP is used by Squirrel.Mac for auto-updates
+ name: '@electron-forge/maker-squirrel',
config: (arch) => ({
- // Path must match S3 publisher's folder structure: releases/darwin/{arch}
- macUpdateManifestBaseUrl: `https://rowboat-desktop-app-releases.s3.amazonaws.com/releases/darwin/${arch}`
+ authors: 'rowboatlabs',
+ description: 'AI coworker with memory',
+ name: `Rowboat-win32-${arch}`,
+ setupExe: `Rowboat-win32-${arch}-${pkg.version}-setup.exe`,
})
+ },
+ {
+ name: '@electron-forge/maker-deb',
+ config: (arch) => ({
+ options: {
+ name: `Rowboat-linux`,
+ bin: "rowboat",
+ description: 'AI coworker with memory',
+ maintainer: 'rowboatlabs',
+ homepage: 'https://rowboatlabs.com'
+ }
+ })
+ },
+ {
+ name: '@electron-forge/maker-rpm',
+ config: {
+ options: {
+ name: `Rowboat-linux`,
+ bin: "rowboat",
+ description: 'AI coworker with memory',
+ homepage: 'https://rowboatlabs.com'
+ }
+ }
+ },
+ {
+ name: '@electron-forge/maker-zip',
+ platform: ["darwin", "win32", "linux"],
}
],
publishers: [
{
- name: '@electron-forge/publisher-s3',
+ name: '@electron-forge/publisher-github',
config: {
- bucket: 'rowboat-desktop-app-releases',
- region: 'us-east-1',
- public: true,
- folder: 'releases' // Creates structure: releases/darwin/{arch}/files (separate builds for arm64 and x64)
+ repository: {
+ owner: 'rowboatlabs',
+ name: 'rowboat'
+ },
+ prerelease: true
}
}
],
diff --git a/apps/x/apps/main/package.json b/apps/x/apps/main/package.json
index 676f7269..c777a237 100644
--- a/apps/x/apps/main/package.json
+++ b/apps/x/apps/main/package.json
@@ -1,31 +1,41 @@
{
- "name": "Rowboat",
+ "name": "rowboat",
+ "productName": "Rowboat",
+ "description": "AI coworker with memory",
"type": "module",
"version": "0.1.0",
"main": ".package/dist/main.cjs",
+ "license": "Apache-2.0",
"scripts": {
"start": "electron .",
"build": "rm -rf dist && tsc && node bundle.mjs",
- "package": "electron-forge package --arch=arm64,x64 --platform=darwin",
- "make": "electron-forge make --arch=arm64,x64 --platform=darwin",
- "publish": "electron-forge publish --arch=arm64,x64 --platform=darwin"
+ "package": "electron-forge package",
+ "make": "electron-forge make"
},
"dependencies": {
"@x/core": "workspace:*",
"@x/shared": "workspace:*",
"chokidar": "^4.0.3",
+ "electron-squirrel-startup": "^1.0.1",
+ "mammoth": "^1.11.0",
+ "papaparse": "^5.5.3",
+ "pdf-parse": "^2.4.5",
"update-electron-app": "^3.1.2",
+ "xlsx": "^0.18.5",
"zod": "^4.2.1"
},
"devDependencies": {
- "@types/node": "^25.0.3",
- "electron": "^39.2.7",
- "esbuild": "^0.24.2",
"@electron-forge/cli": "^7.10.2",
- "@electron-forge/maker-deb": "^7.10.2",
+ "@electron-forge/maker-deb": "^7.11.1",
"@electron-forge/maker-dmg": "^7.10.2",
+ "@electron-forge/maker-rpm": "^7.11.1",
"@electron-forge/maker-squirrel": "^7.10.2",
"@electron-forge/maker-zip": "^7.10.2",
- "@electron-forge/publisher-s3": "^7.10.2"
+ "@electron-forge/publisher-github": "^7.11.1",
+ "@electron-forge/publisher-s3": "^7.10.2",
+ "@types/electron-squirrel-startup": "^1.0.2",
+ "@types/node": "^25.0.3",
+ "electron": "^39.2.7",
+ "esbuild": "^0.24.2"
}
}
\ No newline at end of file
diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts
index 5a7a7bd9..2896ee7a 100644
--- a/apps/x/apps/main/src/ipc.ts
+++ b/apps/x/apps/main/src/ipc.ts
@@ -1,5 +1,7 @@
-import { ipcMain, BrowserWindow } from 'electron';
+import { ipcMain, BrowserWindow, shell } from 'electron';
import { ipc } from '@x/shared';
+import path from 'node:path';
+import os from 'node:os';
import {
connectProvider,
disconnectProvider,
@@ -12,10 +14,12 @@ import { workspace as workspaceShared } from '@x/shared';
import * as mcpCore from '@x/core/dist/mcp/mcp.js';
import * as runsCore from '@x/core/dist/runs/runs.js';
import { bus } from '@x/core/dist/runs/bus.js';
+import { serviceBus } from '@x/core/dist/services/service_bus.js';
import type { FSWatcher } from 'chokidar';
import fs from 'node:fs/promises';
import z from 'zod';
import { RunEvent } from '@x/shared/dist/runs.js';
+import { ServiceEvent } from '@x/shared/dist/service-events.js';
import container from '@x/core/dist/di/container.js';
import { listOnboardingModels } from '@x/core/dist/models/models-dev.js';
import { testModelConnection } from '@x/core/dist/models/models.js';
@@ -24,6 +28,9 @@ import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js';
import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js';
import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js';
import * as composioHandler from './composio-handler.js';
+import { IAgentScheduleRepo } from '@x/core/dist/agent-schedule/repo.js';
+import { IAgentScheduleStateRepo } from '@x/core/dist/agent-schedule/state-repo.js';
+import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedule/runner.js';
type InvokeChannels = ipc.InvokeChannels;
type IPCChannels = ipc.IPCChannels;
@@ -215,6 +222,15 @@ function emitRunEvent(event: z.infer): void {
}
}
+function emitServiceEvent(event: z.infer): void {
+ const windows = BrowserWindow.getAllWindows();
+ for (const win of windows) {
+ if (!win.isDestroyed() && win.webContents) {
+ win.webContents.send('services:events', event);
+ }
+ }
+}
+
export function emitOAuthEvent(event: { provider: string; success: boolean; error?: string }): void {
const windows = BrowserWindow.getAllWindows();
for (const win of windows) {
@@ -234,6 +250,30 @@ export async function startRunsWatcher(): Promise {
});
}
+let servicesWatcher: (() => void) | null = null;
+export async function startServicesWatcher(): Promise {
+ if (servicesWatcher) {
+ return;
+ }
+ servicesWatcher = await serviceBus.subscribe(async (event) => {
+ emitServiceEvent(event);
+ });
+}
+
+export function stopRunsWatcher(): void {
+ if (runsWatcher) {
+ runsWatcher();
+ runsWatcher = null;
+ }
+}
+
+export function stopServicesWatcher(): void {
+ if (servicesWatcher) {
+ servicesWatcher();
+ servicesWatcher = null;
+ }
+}
+
// ============================================================================
// Handler Implementations
// ============================================================================
@@ -384,5 +424,76 @@ export function setupIpcHandlers() {
'composio:execute-action': async (_event, args) => {
return composioHandler.executeAction(args.actionSlug, args.toolkitSlug, args.input);
},
+ // Agent schedule handlers
+ 'agent-schedule:getConfig': async () => {
+ const repo = container.resolve('agentScheduleRepo');
+ try {
+ return await repo.getConfig();
+ } catch {
+ // Return empty config if file doesn't exist
+ return { agents: {} };
+ }
+ },
+ 'agent-schedule:getState': async () => {
+ const repo = container.resolve('agentScheduleStateRepo');
+ try {
+ return await repo.getState();
+ } catch {
+ // Return empty state if file doesn't exist
+ return { agents: {} };
+ }
+ },
+ 'agent-schedule:updateAgent': async (_event, args) => {
+ const repo = container.resolve('agentScheduleRepo');
+ await repo.upsert(args.agentName, args.entry);
+ // Trigger the runner to pick up the change immediately
+ triggerAgentScheduleRun();
+ return { success: true };
+ },
+ 'agent-schedule:deleteAgent': async (_event, args) => {
+ const repo = container.resolve('agentScheduleRepo');
+ const stateRepo = container.resolve('agentScheduleStateRepo');
+ await repo.delete(args.agentName);
+ await stateRepo.deleteAgentState(args.agentName);
+ return { success: true };
+ },
+ // Shell integration handlers
+ 'shell:openPath': async (_event, args) => {
+ let filePath = args.path;
+ if (filePath.startsWith('~')) {
+ filePath = path.join(os.homedir(), filePath.slice(1));
+ } else if (!path.isAbsolute(filePath)) {
+ // Workspace-relative path — resolve against ~/.rowboat/
+ filePath = path.join(os.homedir(), '.rowboat', filePath);
+ }
+ const error = await shell.openPath(filePath);
+ return { error: error || undefined };
+ },
+ 'shell:readFileBase64': async (_event, args) => {
+ let filePath = args.path;
+ if (filePath.startsWith('~')) {
+ filePath = path.join(os.homedir(), filePath.slice(1));
+ } else if (!path.isAbsolute(filePath)) {
+ // Workspace-relative path — resolve against ~/.rowboat/
+ filePath = path.join(os.homedir(), '.rowboat', filePath);
+ }
+ const stat = await fs.stat(filePath);
+ if (stat.size > 10 * 1024 * 1024) {
+ throw new Error('File too large (>10MB)');
+ }
+ const buffer = await fs.readFile(filePath);
+ const ext = path.extname(filePath).toLowerCase();
+ const mimeMap: Record = {
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
+ '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
+ '.bmp': 'image/bmp', '.ico': 'image/x-icon',
+ '.wav': 'audio/wav', '.mp3': 'audio/mpeg', '.m4a': 'audio/mp4',
+ '.ogg': 'audio/ogg', '.flac': 'audio/flac', '.aac': 'audio/aac',
+ '.pdf': 'application/pdf', '.json': 'application/json',
+ '.txt': 'text/plain', '.md': 'text/markdown',
+ };
+ const mimeType = mimeMap[ext] || 'application/octet-stream';
+ return { data: buffer.toString('base64'), mimeType, size: stat.size };
+ },
});
}
diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts
index 6ddab7bc..2a5330ab 100644
--- a/apps/x/apps/main/src/main.ts
+++ b/apps/x/apps/main/src/main.ts
@@ -1,6 +1,14 @@
import { app, BrowserWindow, protocol, net, shell } from "electron";
import path from "node:path";
-import { setupIpcHandlers, startRunsWatcher, startWorkspaceWatcher, stopWorkspaceWatcher } from "./ipc.js";
+import {
+ setupIpcHandlers,
+ startRunsWatcher,
+ startServicesWatcher,
+ startWorkspaceWatcher,
+ stopRunsWatcher,
+ stopServicesWatcher,
+ stopWorkspaceWatcher
+} from "./ipc.js";
import { fileURLToPath, pathToFileURL } from "node:url";
import { dirname } from "node:path";
import { updateElectronApp, UpdateSourceType } from "update-electron-app";
@@ -10,11 +18,16 @@ import { init as initFirefliesSync } from "@x/core/dist/knowledge/sync_fireflies
import { init as initGranolaSync } from "@x/core/dist/knowledge/granola/sync.js";
import { init as initGraphBuilder } from "@x/core/dist/knowledge/build_graph.js";
import { init as initPreBuiltRunner } from "@x/core/dist/pre_built/runner.js";
+import { init as initAgentRunner } from "@x/core/dist/agent-schedule/runner.js";
import { initConfigs } from "@x/core/dist/config/initConfigs.js";
+import started from "electron-squirrel-startup";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
+// run this as early in the main process as possible
+if (started) app.quit();
+
// Path resolution differs between development and production:
const preloadPath = app.isPackaged
? path.join(__dirname, "../preload/dist/preload.js")
@@ -64,6 +77,10 @@ function createWindow() {
const win = new BrowserWindow({
width: 1280,
height: 800,
+ show: false, // Don't show until ready
+ backgroundColor: "#252525", // Prevent white flash (matches dark mode)
+ titleBarStyle: "hiddenInset",
+ trafficLightPosition: { x: 12, y: 12 },
webPreferences: {
// IMPORTANT: keep Node out of renderer
nodeIntegration: false,
@@ -73,6 +90,11 @@ function createWindow() {
},
});
+ // Show window when content is ready to prevent blank screen
+ win.once("ready-to-show", () => {
+ win.show();
+ });
+
// Open external links in system browser (not sandboxed Electron window)
// This handles window.open() and target="_blank" links
win.webContents.setWindowOpenHandler(({ url }) => {
@@ -107,8 +129,8 @@ app.whenReady().then(async () => {
if (app.isPackaged) {
updateElectronApp({
updateSource: {
- type: UpdateSourceType.StaticStorage,
- baseUrl: `https://rowboat-desktop-app-releases.s3.amazonaws.com/releases/${process.platform}/${process.arch}`,
+ type: UpdateSourceType.ElectronPublicUpdateService,
+ repo: "rowboatlabs/rowboat",
},
notifyUser: true, // Shows native dialog when update is available
});
@@ -131,6 +153,9 @@ app.whenReady().then(async () => {
// start runs watcher
startRunsWatcher();
+ // start services watcher
+ startServicesWatcher();
+
// start gmail sync
initGmailSync();
@@ -149,6 +174,9 @@ app.whenReady().then(async () => {
// start pre-built agent runner
initPreBuiltRunner();
+ // start background agent runner (scheduled agents)
+ initAgentRunner();
+
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
@@ -165,4 +193,6 @@ app.on("window-all-closed", () => {
app.on("before-quit", () => {
// Clean up watcher on app quit
stopWorkspaceWatcher();
-});
\ No newline at end of file
+ stopRunsWatcher();
+ stopServicesWatcher();
+});
diff --git a/apps/x/apps/renderer/index.html b/apps/x/apps/renderer/index.html
index 1803a850..856065c2 100644
--- a/apps/x/apps/renderer/index.html
+++ b/apps/x/apps/renderer/index.html
@@ -5,6 +5,22 @@
Rowboat
+
+
diff --git a/apps/x/apps/renderer/public/logo-only.png b/apps/x/apps/renderer/public/logo-only.png
new file mode 100644
index 00000000..e2fd6386
Binary files /dev/null and b/apps/x/apps/renderer/public/logo-only.png differ
diff --git a/apps/x/apps/renderer/src/App.css b/apps/x/apps/renderer/src/App.css
index 4fd02863..991236ea 100644
--- a/apps/x/apps/renderer/src/App.css
+++ b/apps/x/apps/renderer/src/App.css
@@ -123,6 +123,9 @@
--sidebar-accent-foreground: var(--text-color, oklch(0.205 0 0));
--sidebar-border: var(--sub-alt-color, oklch(0.922 0 0));
--sidebar-ring: var(--main-color, oklch(0.708 0 0));
+ --scrollbar-track: oklch(0.95 0 0);
+ --scrollbar-thumb: oklch(0.75 0 0);
+ --scrollbar-thumb-hover: oklch(0.65 0 0);
}
.dark {
@@ -157,15 +160,39 @@
--sidebar-accent-foreground: var(--text-color, oklch(0.985 0 0));
--sidebar-border: var(--sub-alt-color, oklch(1 0 0 / 10%));
--sidebar-ring: var(--main-color, oklch(0.556 0 0));
+ --scrollbar-track: oklch(0.2 0 0);
+ --scrollbar-thumb: oklch(0.4 0 0);
+ --scrollbar-thumb-hover: oklch(0.5 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
+ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
+ scrollbar-width: thin;
}
+
body {
@apply bg-background text-foreground;
}
+
+ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background: var(--scrollbar-track);
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: var(--scrollbar-thumb);
+ border-radius: 4px;
+ }
+
+ ::-webkit-scrollbar-thumb:hover {
+ background: var(--scrollbar-thumb-hover);
+ }
}
/* Markdown content base styles for Streamdown/MessageResponse */
@@ -228,6 +255,15 @@
}
}
+/* Titlebar drag regions for frameless window */
+.titlebar-drag-region {
+ -webkit-app-region: drag;
+}
+
+.titlebar-no-drag {
+ -webkit-app-region: no-drag;
+}
+
.graph-view {
background-color: var(--background);
user-select: none;
diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx
index 25ab8031..bc9bb6cc 100644
--- a/apps/x/apps/renderer/src/App.tsx
+++ b/apps/x/apps/renderer/src/App.tsx
@@ -6,20 +6,20 @@ import type { LanguageModelUsage, ToolUIPart } from 'ai';
import './App.css'
import z from 'zod';
import { Button } from './components/ui/button';
-import { CheckIcon, LoaderIcon, ArrowUp, PanelRightIcon, SquarePen, Square } from 'lucide-react';
+import { CheckIcon, LoaderIcon, ArrowUp, PanelLeftIcon, PanelRightIcon, Square, X, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import { MarkdownEditor } from './components/markdown-editor';
import { ChatInputBar } from './components/chat-button';
import { ChatSidebar } from './components/chat-sidebar';
import { GraphView, type GraphEdge, type GraphNode } from '@/components/graph-view';
import { useDebounce } from './hooks/use-debounce';
-import { SidebarIcon } from '@/components/sidebar-icon';
import { SidebarContentPanel } from '@/components/sidebar-content';
import { SidebarSectionProvider, type ActiveSection } from '@/contexts/sidebar-context';
import {
Conversation,
ConversationContent,
ConversationEmptyState,
+ ScrollPositionPreserver,
} from '@/components/ai-elements/conversation';
import {
Message,
@@ -43,13 +43,17 @@ import { ToolPermissionRequestEvent, AskHumanRequestEvent } from '@x/shared/src/
import {
SidebarInset,
SidebarProvider,
- SidebarTrigger,
+ useSidebar,
} from "@/components/ui/sidebar"
import { TooltipProvider } from "@/components/ui/tooltip"
-import { Separator } from "@/components/ui/separator"
import { Toaster } from "@/components/ui/sonner"
import { stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-links'
import { OnboardingModal } from '@/components/onboarding-modal'
+import { BackgroundTaskDetail } from '@/components/background-task-detail'
+import { FileCardProvider } from '@/contexts/file-card-context'
+import { MarkdownPreOverride } from '@/components/ai-elements/markdown-code-override'
+import { AgentScheduleConfig } from '@x/shared/dist/agent-schedule.js'
+import { AgentScheduleState } from '@x/shared/dist/agent-schedule-state.js'
type DirEntry = z.infer
type RunEventType = z.infer
@@ -106,6 +110,8 @@ const toToolState = (status: ToolCall['status']): ToolState => {
}
}
+const streamdownComponents = { pre: MarkdownPreOverride }
+
const DEFAULT_SIDEBAR_WIDTH = 256
const wikiLinkRegex = /\[\[([^[\]]+)\]\]/g
const graphPalette = [
@@ -354,7 +360,7 @@ function ChatInputInner({
}, [controller])
return (
-