Skip to content
Merged
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- Do not invent Runta Cloud Agents routes or event framing. Record assumptions in `API_INTEGRATION.md` and require route/transport injection.
- Mock fixtures belong under `src/clients/mock`, never in React components. Mock computer UI must remain visibly labeled.
- Default to the light theme and preserve both theme token sets.
- Read and follow `DESIGN_SYSTEM.md` for every UI change. Apply its visible-element admission rule and border checklist before adding permanent controls or decoration.
- Use strict TypeScript; do not add `any` escapes.
- Run `npm run typecheck`, `npm run lint`, `npm test`, `npm run build`, and packaging/smoke checks for release-facing changes.
- Use Conventional Commits. Changes in this `runta-dev` repository must go through a feature branch and pull request.
- Runta Crew is temporarily not onboarded to Runta Review. Do not trigger, wait for, or treat Runta Review as a merge gate for this repository. GitHub pull requests and the repository CI workflow are the required review/delivery path until this rule is explicitly changed.
4 changes: 4 additions & 0 deletions API_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ The backend contract must support:
| Client operation | Required behavior |
| --- | --- |
| `listAgents`, `getAgent`, `createAgent` | Named agents, role/goal, status, timestamps, unread/approval counters, computer association |
| `updateAgent`, `deleteAgent`, `duplicateAgent`, `setAgentUnread` | Optimistic concurrency/version, deletion cleanup, duplication semantics and per-user preferences |
| `listConversations`, `getConversation` | Stable conversation identity and ordered message history |
| `sendMessage` | Idempotency key, accepted message, and subsequent stream identity |
| `reactToMessage` | Supported reaction vocabulary, toggle/idempotency behavior and multi-device counts |
| `subscribeToConversationEvents` | Resume cursor, ordering, heartbeat, message deltas/completion, activity, approvals, connection events |
| `listApprovalRequests`, `respondToApproval` | Explicit action scope, decision, optional note, actor, audit timestamp, single-use semantics |
| `getComputer` | Runtime state, active app/tool, preview availability, supported actions |
Expand Down Expand Up @@ -39,3 +41,5 @@ The domain currently understands `message.created`, `message.delta`, `message.co
## Credential handling

Tokens entered in Settings are encrypted using Electron `safeStorage` and written with user-only file permissions. The preload exposes `has` and `set`, not credential reads. Production API requests should be made by a main-process broker that decrypts only for the outbound request.

Local file selection currently returns only an opaque ID and safe metadata to the renderer. The main process retains the path in memory. `HttpCloudAgentsClient` deliberately rejects these local selections until the backend defines upload creation/finalization, size limits, checksums, and how an opaque desktop selection becomes a cloud attachment ID.
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ React renderer
└── HttpCloudAgentsClient (contract-ready adapter)
```

The renderer runs with `contextIsolation: true`, `nodeIntegration: false`, and `sandbox: true`. It cannot access the filesystem, Node.js, arbitrary Electron APIs, or arbitrary IPC channels. The preload exposes only application version, HTTP(S) external navigation, non-secret settings, and write/exists credential operations. It never exposes the stored credential value.
The renderer runs with `contextIsolation: true`, `nodeIntegration: false`, and `sandbox: true`. It cannot access the filesystem, Node.js, arbitrary Electron APIs, or arbitrary IPC channels. The preload exposes only application version, HTTP(S) external navigation, non-secret settings, write/exists credential operations, native file selection metadata, and bounded notification operations. It never exposes the stored credential value or selected filesystem paths.

## Cloud Agents boundary

Expand Down
49 changes: 49 additions & 0 deletions DESIGN_SYSTEM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Runta Crew Design System

Runta Crew is a work surface, not a dashboard. Its interface should feel quiet until the user needs to act.

## Visible-element admission rule

Every permanently visible element must do at least one of the following:

1. Enable an immediate user action.
2. Communicate state that changes the user's next decision.
3. Identify the current context.
4. Explain an error or required intervention.

If removing an element does not make a common task harder, less safe, or ambiguous, remove it. Do not add labels, badges, borders, cards, shadows, status dots, helper copy, or decorative artwork merely to make an area feel complete.

## Whitespace

- Prefer open space over filler UI. Empty space establishes hierarchy and keeps conversation content primary.
- Do not fill blank areas with metrics, onboarding copy, illustrations, or secondary navigation unless the current task requires them.
- Keep controls close to the object they operate on; do not create extra toolbars for infrequent actions.

## Border usage

- Borders communicate structure or interactivity. They are not decoration.
- Inputs may keep a subtle border because it makes editability immediately clear.
- A stationary conversation header has no bottom border. Show a `0.5px` hairline only after content scrolls beneath it.
- The sidebar boundary uses a low-contrast `0.5px` hairline. It separates regions without becoming a visual column.
- Avoid nested borders. Prefer spacing or a single soft surface before adding another outline.
- Do not put borders around self-explanatory icon actions located in an established action area.
- Use stronger borders only for safety-critical boundaries, focused inputs, errors, approvals, or selected controls.

## Immediate comprehension

- A visible control must be understandable at a glance from its icon, label, position, and current context.
- Do not rely on users discovering what a permanent control means through trial and error.
- Use icon-only controls only for conventional actions or when placement makes the meaning unambiguous. Always provide an accessible name.
- Prefer direct language: describe the action (`Open computer`, `Delete agent`) rather than an internal system concept.
- Do not show raw infrastructure state unless it affects what the user can do next.

## UI review checklist

Before shipping a UI change, verify:

- Can any new visible element be removed without reducing clarity, safety, or capability?
- Is any state represented twice?
- Does every border have a structural, interactive, or safety purpose?
- Is the default state quieter than its hover, focus, active, error, and approval states?
- Can a first-time user understand every persistent control without documentation?
- Does the layout preserve generous whitespace at both the default and minimum window sizes?
33 changes: 33 additions & 0 deletions FEATURE_PARITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Grok Bot → Runta Crew feature parity

This matrix compares the public/reconstructed Grok Bot 0.18 product surface with Runta Crew. The reference repository has no reusable upstream source license, so entries describe behavior to implement independently, not code to copy.

Legend: **Done** ships in the current desktop client; **Local next** can be implemented without a backend contract; **API blocked** requires a confirmed Runta Cloud Agents contract; **Excluded** is intentionally outside Runta Crew's product direction.

| Area | Reference surface | Runta Crew status | Next contract or work |
| --- | --- | --- | --- |
| Multi-agent sidebar | Named agents, unread state, row actions | **Done:** named agents, status, badges, search, edit, pin, duplicate, read state and confirmed delete | **Local next:** hide/restore and bulk organization |
| Command palette | Root commands plus agent/message/link search | **Done:** keyboard palette, agent switching, create/settings/computer commands | **Local next:** indexed message/link search after durable history exists |
| Conversation | Streaming turns, thinking/activity, reconnect | **Done:** typed messages, streaming deltas/completion, activity timeline, errors/reconnect | **API blocked:** durable cursor/replay, cancellation, retries, pagination |
| Agent computer | Remote box overlay and takeover | **Done:** status, active app, clearly labeled safe mock, Open/Take over UX | **API blocked:** signed ingress/WebRTC/VNC session, arbitration, audit |
| Approvals | Tool permission scope and user decision | **Done:** scoped Allow once/Deny with note | **API blocked:** expiry, revocation, policy modes, durable audit |
| Attachments | File/image/link attachment gateway and downloads | **Done:** typed native chooser, opaque IDs, metadata preview, removal, 25 MB validation, mock message parts | **API blocked:** upload, signed download, remote file identity |
| Reactions | Message reaction root and acknowledgement | **Done:** typed useful/needs-work reactions, optimistic mock event updates and accessible controls | **API blocked:** persistence and multi-device sync |
| Notifications | OS notification manager and dock badge | **Done:** typed, preference-aware, focus-aware OS notification bridge and unread dock badge | **API blocked:** background event delivery when the app is closed |
| Deep links | Single-instance app routing from external surfaces | **Done:** strict `runta-crew://agent/<id>` parsing and typed renderer navigation | **API blocked:** canonical web-to-desktop link issuance |
| Agent lifecycle | Rename, delete, duplicate, preferences | **Done:** mock lifecycle, pin/read preferences and destructive confirmation UI | **API blocked:** canonical mutation and concurrency semantics |
| Plugins/MCP | Plugin lifecycle, OAuth, MCP tools | **API blocked:** Runta plugin catalog, OAuth and agent capability contract | Do not reuse Cursor/xAI plugin services |
| Skills/routines | Saved repeatable work and proactive routines | **API blocked:** skill schema, scheduler, ownership and execution history | Desktop management UI follows backend contract |
| Multi-agent handoff | Subagents/group members and shared context | **API blocked:** relationship, handoff, permissions and event model | Keep `ActivityEvent.kind = handoff` as the UI seam |
| Authentication | Cursor account/session funnel | **API blocked:** Runta account/device authorization | **Excluded:** Cursor account and machine identity |
| Local inference router | Cursor/Claude/Codex/OpenRouter routing extension | **Excluded** | Runta Crew connects to Runta Cloud Agents rather than routing local inference |
| Local Docker box | Optional replacement for remote box | **Excluded for product MVP** | Runta Runtime is the cloud-computer substrate |
| Telemetry/updater | Upstream services and release feeds | **Excluded until approved** | Ports exist; require Runta privacy policy, signing and update infrastructure |

## Delivery order

1. Local desktop completeness: agent row actions, attachments, reactions, OS notifications, deep links.
2. Durable Cloud Agents foundation: auth, agents, conversations, resumable events, approvals.
3. Real computer transport: short-lived session descriptors, preview, takeover, audit.
4. Extensibility: plugins/MCP, skills, routines, schedules.
5. Collaboration: handoffs, shared computers, organization policy, multi-device sync.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ Runta Crew is the desktop client for creating, messaging, and supervising Runta
## What works today

- Search, switch, and create named agents with roles and goals.
- Edit, pin, duplicate, mark read/unread, and safely delete agents.
- Display working, idle, approval-required, and offline states.
- Persistent-style conversations with user, agent, system, and activity data models.
- Mock streaming responses and structured browser/file activity.
- Native attachment selection with opaque IDs, safe metadata previews, removal, and size validation.
- Useful/needs-work message reactions.
- Scoped approval review with explicit Allow once / Deny actions and notes.
- Cloud computer status plus safe mock Open / Take over surfaces.
- Connection, theme, notification, credential, and About settings.
- OS-encrypted credential storage through Electron `safeStorage`.
- Focus-aware OS notifications and macOS unread dock badges.
- Single-instance `runta-crew://agent/<id>` deep links.
- macOS native window/menu, DMG/ZIP packaging, and a native packaged-app smoke check.

## Development
Expand Down Expand Up @@ -53,6 +58,7 @@ Packaged artifacts are written under `release/`. The app is ad-hoc signed for lo
- `src/ui`: presentation and focused interaction components.

See [ARCHITECTURE.md](./ARCHITECTURE.md) for process and security boundaries.
See [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md) for the UI necessity, whitespace, border, and immediate-comprehension rules.

## Relationship to the reference project

Expand Down
Binary file modified docs/screenshots/main-window.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/minimum-window.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 56 additions & 5 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import { app, BrowserWindow, ipcMain, Menu, safeStorage, shell } from "electron";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { app, BrowserWindow, dialog, ipcMain, Menu, Notification, safeStorage, shell } from "electron";
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { extname, join, basename } from "node:path";
import { randomUUID } from "node:crypto";
import type { AppSettings } from "../../src/shared/desktop";

const isDev = Boolean(process.env.VITE_DEV_SERVER_URL);
const devServerUrl = process.env.ELECTRON_RENDERER_URL ?? process.env.VITE_DEV_SERVER_URL;
const isDev = Boolean(devServerUrl);
const credentialFile = () => join(app.getPath("userData"), "credentials.bin");
const settingsFile = () => join(app.getPath("userData"), "settings.json");
let settings: AppSettings = { endpoint: "", theme: "light", notifications: true };
const selectedAttachmentPaths = new Map<string, string>();
let mainWindow: BrowserWindow | undefined;
let pendingDeepLinkAgentId: string | undefined;

function agentIdFromDeepLink(value: string): string | undefined {
try { const url = new URL(value); const id = url.protocol === "runta-crew:" && url.hostname === "agent" ? decodeURIComponent(url.pathname.replace(/^\//, "")) : ""; return /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(id) ? id : undefined; } catch { return undefined; }
}
function openAgentDeepLink(value: string) {
const agentId = agentIdFromDeepLink(value); if (!agentId) return;
pendingDeepLinkAgentId = agentId;
if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.show(); mainWindow.focus(); mainWindow.webContents.send("deep-link:agent", agentId); pendingDeepLinkAgentId = undefined; }
}

function mediaTypeForPath(path: string): string {
const types: Record<string, string> = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".pdf": "application/pdf", ".txt": "text/plain", ".md": "text/markdown", ".json": "application/json", ".csv": "text/csv" };
return types[extname(path).toLowerCase()] ?? "application/octet-stream";
}

function loadSettings(): AppSettings {
if (!existsSync(settingsFile())) return settings;
Expand All @@ -31,7 +50,8 @@ function createWindow() {
contextIsolation: true, nodeIntegration: false, sandbox: true,
},
});
if (isDev && process.env.VITE_DEV_SERVER_URL) void window.loadURL(process.env.VITE_DEV_SERVER_URL);
mainWindow = window; window.on("closed", () => { if (mainWindow === window) mainWindow = undefined; });
if (isDev && devServerUrl) void window.loadURL(devServerUrl);
else void window.loadFile(join(__dirname, "../renderer/index.html"));
window.webContents.setWindowOpenHandler(({ url }) => {
try { const parsed = new URL(url); if (parsed.protocol === "https:" || parsed.protocol === "http:") void shell.openExternal(parsed.toString()); } catch { /* deny malformed URLs */ }
Expand All @@ -42,15 +62,24 @@ function createWindow() {
if (url !== current) event.preventDefault();
});
window.webContents.once("did-finish-load", () => {
if (pendingDeepLinkAgentId) { window.webContents.send("deep-link:agent", pendingDeepLinkAgentId); pendingDeepLinkAgentId = undefined; }
const smokeMarker = process.env.RUNTA_CREW_SMOKE_MARKER;
if (smokeMarker) { writeFileSync(smokeMarker, "ready\n"); app.quit(); return; }
const screenshotPath = process.env.RUNTA_CREW_SCREENSHOT_PATH;
if (screenshotPath) globalThis.setTimeout(() => { void window.webContents.capturePage().then((image) => { writeFileSync(screenshotPath, image.toPNG()); app.quit(); }); }, 1200);
});
}

const hasSingleInstanceLock = app.requestSingleInstanceLock();
if (!hasSingleInstanceLock) app.quit();
else {
app.on("second-instance", (_event, argv) => { const deepLink = argv.find((value) => value.startsWith("runta-crew://")); if (deepLink) openAgentDeepLink(deepLink); else { mainWindow?.show(); mainWindow?.focus(); } });
app.on("open-url", (event, url) => { event.preventDefault(); openAgentDeepLink(url); });
}

app.whenReady().then(() => {
settings = loadSettings();
if (app.isPackaged) app.setAsDefaultProtocolClient("runta-crew");
Menu.setApplicationMenu(Menu.buildFromTemplate([
{ label: "Runta Crew", submenu: [{ role: "about" }, { type: "separator" }, { role: "quit" }] },
{ label: "Edit", submenu: [{ role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }, { role: "selectAll" }] },
Expand Down Expand Up @@ -78,3 +107,25 @@ ipcMain.handle("credentials:set", (_event, token: string | null) => {
writeFileSync(credentialFile(), safeStorage.encryptString(token), { mode: 0o600 });
return true;
});
ipcMain.handle("attachments:choose", async () => {
const result = await dialog.showOpenDialog({ title: "Attach files to your message", properties: ["openFile", "multiSelections"], filters: [{ name: "Supported files", extensions: ["png", "jpg", "jpeg", "gif", "webp", "pdf", "txt", "md", "json", "csv"] }] });
if (result.canceled) return [];
const attachments = result.filePaths.flatMap((path) => {
const size = statSync(path).size;
if (size > 25 * 1024 * 1024) return [];
const id = randomUUID(); selectedAttachmentPaths.set(id, path);
return [{ id, name: basename(path), size, mediaType: mediaTypeForPath(path) }];
});
if (attachments.length !== result.filePaths.length) await dialog.showMessageBox({ type: "warning", title: "Some files were not attached", message: "Runta Crew supports files up to 25 MB in this preview." });
return attachments;
});
ipcMain.handle("notifications:show", (event, value: { title?: unknown; body?: unknown }) => {
const title = typeof value?.title === "string" ? value.title.slice(0, 120) : "";
const body = typeof value?.body === "string" ? value.body.slice(0, 500) : "";
const sourceWindow = BrowserWindow.fromWebContents(event.sender);
if (!settings.notifications || !title || !body || !Notification.isSupported() || sourceWindow?.isFocused()) return false;
new Notification({ title, body }).show(); return true;
});
ipcMain.handle("notifications:setBadge", (_event, count: number) => {
if (process.platform === "darwin" && app.dock) app.dock.setBadge(Number.isSafeInteger(count) && count > 0 ? String(Math.min(count, 99)) : "");
});
11 changes: 11 additions & 0 deletions electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ const bridge: DesktopBridge = {
has: () => ipcRenderer.invoke("credentials:has"),
set: (token) => ipcRenderer.invoke("credentials:set", token),
},
attachments: { choose: () => ipcRenderer.invoke("attachments:choose") },
notifications: {
show: (notification) => ipcRenderer.invoke("notifications:show", notification),
setBadge: (count) => ipcRenderer.invoke("notifications:setBadge", count),
},
deepLinks: {
onOpenAgent: (listener) => {
const handler = (_event: Electron.IpcRendererEvent, agentId: string) => listener(agentId);
ipcRenderer.on("deep-link:agent", handler); return () => ipcRenderer.removeListener("deep-link:agent", handler);
},
},
};

contextBridge.exposeInMainWorld("runtaCrew", bridge);
Loading
Loading