Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/cross-platform-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Cross-Platform CI

on:
push:
branches:
- main
pull_request:

jobs:
build-and-package-smoke:
name: Build + Package Smoke (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: |
package-lock.json
desktop/package-lock.json

- name: Install root dependencies
run: npm ci

- name: Install desktop dependencies
run: npm ci
working-directory: desktop

- name: Build gateway/CLI
run: npm run build

- name: Build desktop
run: npm -C desktop run build

- name: Bundle gateway resources
run: npm -C desktop run bundle-gateway

- name: Linux package smoke
if: runner.os == 'Linux'
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
run: npx --yes electron-builder --projectDir desktop --linux --dir

- name: macOS package smoke
if: runner.os == 'macOS'
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
run: npx --yes electron-builder --projectDir desktop --mac --dir --config.mac.notarize=false
46 changes: 38 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
[![GitHub stars](https://img.shields.io/github/stars/suitedaces/dorabot)](https://github.com/suitedaces/dorabot)
[![GitHub release](https://img.shields.io/github/v/release/suitedaces/dorabot)](https://github.com/suitedaces/dorabot/releases/latest)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![macOS](https://img.shields.io/badge/platform-macOS-lightgrey)](https://github.com/suitedaces/dorabot/releases/latest)
[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Ubuntu-lightgrey)](https://github.com/suitedaces/dorabot/releases/latest)

Works with your existing Claude Code or OpenAI Codex subscription. No extra API key needed.

[**Download for macOS**](https://github.com/suitedaces/dorabot/releases/latest) · [Website](https://dora.so) · [Discord](https://discord.gg/FH99jkvMz) · [Demo](https://github.com/suitedaces/dorabot/releases/download/v0.2.3/annotated-demo-telegram.mp4)
[**Download for macOS**](https://github.com/suitedaces/dorabot/releases/latest) · [Ubuntu from source](#build-from-source) · [Website](https://dora.so) · [Discord](https://discord.gg/FH99jkvMz) · [Demo](https://github.com/suitedaces/dorabot/releases/download/v0.2.3/annotated-demo-telegram.mp4)

</div>

Expand Down Expand Up @@ -54,7 +54,7 @@ Same agent on WhatsApp, Telegram, and Slack. Send text, photos, voice, documents

## Skills & MCP Servers

Built-in skills for GitHub, email, macOS, PR review, agent swarms. Browse 56k+ community skills. Connect 7,300+ MCP servers via Smithery.
Built-in skills for GitHub, email, desktop automation, PR review, and agent swarms. Browse 56k+ community skills. Connect 7,300+ MCP servers via Smithery.

<img src="gifs/extensions.gif" width="800" />

Expand All @@ -65,21 +65,34 @@ Built-in skills for GitHub, email, macOS, PR review, agent swarms. Browse 56k+ c
- **Multimodal.** Send images, screenshots, diagrams. The agent sees them.
- **Multi-pane workspace.** Split panes (Cmd+D), parallel agents, streaming responses.
- **Auto-update.** Signed, notarized, one-click updates.
- **Local-only.** No cloud relay. Your data stays on your Mac.
- **Local-only.** No cloud relay. Your data stays on your machine.

## Quick Start

### Download

[**Download the macOS app**](https://github.com/suitedaces/dorabot/releases/latest) -- open the DMG, drag to Applications. Onboarding walks you through setup.

**Requires:** macOS + a Claude Code or OpenAI Codex subscription (or any API key: Claude, OpenAI, MiniMax).
**Requires:** macOS or Ubuntu + a Claude Code or OpenAI Codex subscription (or any API key: Claude, OpenAI, MiniMax).

### Build from source

One-liner (fresh clone, macOS):

```bash
git clone https://github.com/suitedaces/dorabot.git && cd dorabot && bash scripts/install.sh
```

One-liner (fresh clone, Ubuntu):

```bash
git clone https://github.com/suitedaces/dorabot.git && cd dorabot && bash scripts/install.sh
```

If you already cloned the repo:

```bash
git clone https://github.com/suitedaces/dorabot.git && cd dorabot
npm install && npm run build && npm link
bash scripts/install.sh
```

```bash
Expand All @@ -89,6 +102,23 @@ dorabot -i # interactive terminal
dorabot -m "message" # one-off question
```

### Run in browser (no global install)

```bash
npm run dev:web
```

- Open `http://localhost:5173`
- Browser gateway endpoint is `ws://127.0.0.1:18889` (proxied from local Unix socket)

### Ubuntu notes

- Installer script includes Ubuntu deps: `libnotify-bin`, `gnome-screenshot`
- Package desktop app: `npm -C desktop run package:linux`
- Optional desktop integrations:
- Notifications: `notify-send`
- Screenshots: one of `gnome-screenshot`, `grim`, `import`, `maim`, `scrot`

## Personalization

The `onboard` skill interviews you and builds:
Expand All @@ -103,7 +133,7 @@ All files live in `~/.dorabot/workspace/`. Edit directly or let the agent manage

## Security

Local-only, no telemetry. Scoped file access (sensitive dirs blocked). Token-authenticated gateway. Configurable tool approval per channel. macOS native sandbox.
Local-only, no telemetry. Scoped file access (sensitive dirs blocked). Token-authenticated gateway. Configurable tool approval per channel. Desktop sandboxing where supported by platform/runtime.

## FAQ

Expand Down
12 changes: 12 additions & 0 deletions desktop/electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,15 @@ dmg:
y: 220
type: link
path: /Applications

linux:
category: Development
maintainer: dorabot team <hello@dora.so>
synopsis: dorabot desktop control center
target:
- target: AppImage
arch:
- x64
- target: deb
arch:
- x64
14 changes: 11 additions & 3 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@
"name": "dorabot-desktop",
"version": "0.2.13",
"description": "Desktop control center for dorabot",
"author": "dorabot team <hello@dora.so>",
"homepage": "https://github.com/suitedaces/dorabot",
"main": "out/main/main.js",
"scripts": {
"dev": "electron-vite dev",
"web": "vite",
"build": "electron-vite build",
"preview": "electron-vite preview",
"bundle-gateway": "cd .. && npm run build && bash scripts/bundle-gateway.sh",
"package": "npm run bundle-gateway && electron-vite build && electron-builder --mac && npm run install-app",
"package:arm64": "npm run bundle-gateway && electron-vite build && electron-builder --mac --arm64 && npm run install-app",
"package:universal": "npm run bundle-gateway && electron-vite build && electron-builder --mac --universal && npm run install-app",
"package": "npm run package:mac",
"package:mac": "npm run bundle-gateway && electron-vite build && electron-builder --mac && npm run install-app",
"package:mac:arm64": "npm run bundle-gateway && electron-vite build && electron-builder --mac --arm64 && npm run install-app",
"package:mac:universal": "npm run bundle-gateway && electron-vite build && electron-builder --mac --universal && npm run install-app",
"package:linux": "npm run bundle-gateway && electron-vite build && electron-builder --linux AppImage deb",
"package:all": "npm run bundle-gateway && electron-vite build && electron-builder --mac --linux AppImage deb",
"package:arm64": "npm run package:mac:arm64",
"package:universal": "npm run package:mac:universal",
"install-app": "APP=$(find release -maxdepth 2 -name 'dorabot.app' -type d | head -1) && cp -R \"$APP\" /Applications/ && echo \"Installed $APP to /Applications/\"",
"typecheck": "tsc --noEmit"
},
Expand Down
117 changes: 115 additions & 2 deletions desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import {
MessageSquare, Radio, Zap, Brain, Settings2,
Sparkles, LayoutGrid, Loader2, Star,
Expand Down Expand Up @@ -99,6 +109,8 @@ export default function App() {
const [draggingTab, setDraggingTab] = useState<Tab | null>(null);
const { theme, toggle: toggleTheme } = useTheme();
const [updateState, setUpdateState] = useState<UpdateState>({ status: 'idle' });
const [sessionToDelete, setSessionToDelete] = useState<(typeof gw.sessions)[number] | null>(null);
const [deletingSession, setDeletingSession] = useState(false);
const notify = useCallback((body: string) => {
const api = (window as any).electronAPI;
if (api?.notify) {
Expand Down Expand Up @@ -394,6 +406,47 @@ export default function App() {
});
}, [gw.sessions, tabState]);

const handleConfirmDeleteSession = useCallback(async () => {
if (!sessionToDelete || deletingSession) return;
setDeletingSession(true);
try {
const sessionKey = `${sessionToDelete.channel || 'desktop'}:${sessionToDelete.chatType || 'dm'}:${sessionToDelete.chatId || sessionToDelete.id}`;

const tabsToClose = tabState.tabs
.filter(t => isChatTab(t) && (t.sessionId === sessionToDelete.id || t.sessionKey === sessionKey))
.map(t => t.id);
for (const tabId of tabsToClose) tabState.closeTab(tabId);

const result = await gw.deleteSession(sessionToDelete.id);
if (!result?.deleted) {
throw new Error('Session was not deleted.');
}

// Drop in-memory routing for this chat so new traffic starts a fresh session.
if (sessionToDelete.chatId) {
try {
await gw.rpc('sessions.reset', {
channel: sessionToDelete.channel || 'desktop',
chatId: sessionToDelete.chatId,
});
} catch {
// best-effort cleanup only
}
}

toast.success('Session closed', {
description: `${sessionToDelete.senderName || sessionToDelete.chatId || sessionToDelete.id.slice(0, 8)} removed from history.`,
});
setSessionToDelete(null);
} catch (err) {
toast.error('Failed to close session', {
description: err instanceof Error ? err.message : String(err),
});
} finally {
setDeletingSession(false);
}
}, [sessionToDelete, deletingSession, tabState, gw]);

const handleNavClick = useCallback((navId: TabType) => {
if (navId === 'chat') {
// Task nav should open a fresh chat unless a blank draft chat already exists.
Expand Down Expand Up @@ -916,8 +969,10 @@ export default function App() {
const isVisible = !isActive && visibleSessionIds.has(s.id);
const unread = unreadBySessionId[s.id] || 0;
return (
<button
<div
key={s.id}
role="button"
tabIndex={0}
className={`flex items-center gap-1.5 w-full px-2.5 py-1 rounded-md text-[10px] transition-colors ${
isActive
? 'bg-secondary text-foreground'
Expand All @@ -926,6 +981,12 @@ export default function App() {
: 'text-muted-foreground hover:bg-secondary/50'
}`}
onClick={() => handleViewSession(s.id, s.channel, s.chatId, s.chatType)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleViewSession(s.id, s.channel, s.chatId, s.chatType);
}
}}
title={`${s.channel || 'desktop'} | ${s.messageCount} msgs | ${new Date(s.updatedAt).toLocaleString()}`}
>
<span className="w-3 h-3 shrink-0 flex items-center justify-center">{channelIcon(s.channel)}</span>
Expand All @@ -944,7 +1005,22 @@ export default function App() {
{new Date(s.updatedAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
</span>
)}
</button>
<button
type="button"
className="ml-0.5 shrink-0 rounded p-0.5 text-muted-foreground hover:text-destructive hover:bg-secondary/70 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title={s.activeRun ? 'Cannot close while agent is running' : 'Close session'}
aria-label={s.activeRun ? 'Cannot close while agent is running' : 'Close session'}
disabled={!!s.activeRun}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (s.activeRun) return;
setSessionToDelete(s);
}}
>
<X className="w-3 h-3" />
</button>
</div>
);
})}
</ScrollArea>
Expand Down Expand Up @@ -1033,6 +1109,43 @@ export default function App() {
</>
)}
</ResizablePanelGroup>

<AlertDialog
open={!!sessionToDelete}
onOpenChange={(open) => {
if (!open && !deletingSession) setSessionToDelete(null);
}}
>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogTitle className="text-sm">Close this session?</AlertDialogTitle>
<AlertDialogDescription className="text-xs">
This will remove the session from saved history.
</AlertDialogDescription>
{sessionToDelete && (
<div className="w-full rounded border border-border bg-muted/40 px-2 py-1.5 text-[10px] text-muted-foreground">
{sessionToDelete.senderName || sessionToDelete.chatId || sessionToDelete.id.slice(0, 12)}
</div>
)}
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="h-7 text-xs" disabled={deletingSession}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
className="h-7 text-xs"
variant="destructive"
disabled={deletingSession}
onClick={(e) => {
e.preventDefault();
void handleConfirmDeleteSession();
}}
>
{deletingSession ? 'Closing...' : 'Close session'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</TooltipProvider>
);
}
Loading