Skip to content
Merged

0.9.0 #127

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
eb76212
refac
tjbck Jul 10, 2026
444b4fa
refac
tjbck Jul 10, 2026
0bcf0a3
refac
tjbck Jul 10, 2026
81bb380
refac
tjbck Jul 10, 2026
55b39e3
refac
tjbck Jul 10, 2026
b133e8f
refac
tjbck Jul 10, 2026
5146bbe
refac
tjbck Jul 10, 2026
fdb9c45
refac
tjbck Jul 10, 2026
c186284
refac
tjbck Jul 10, 2026
622e1da
refac
tjbck Jul 10, 2026
4181e68
refac
tjbck Jul 10, 2026
96bfd8e
refac
tjbck Jul 10, 2026
0cc9507
refac
tjbck Jul 10, 2026
0216e55
refac
tjbck Jul 10, 2026
3607b2a
refac
tjbck Jul 11, 2026
d04ef24
refac
tjbck Jul 11, 2026
bbf23b3
refac
tjbck Jul 11, 2026
53909d2
refac
tjbck Jul 11, 2026
0e0c0f7
refac
tjbck Jul 11, 2026
de7a73a
refac
tjbck Jul 11, 2026
0159afd
refac
tjbck Jul 11, 2026
3392650
refac
tjbck Jul 11, 2026
ba20750
refac
tjbck Jul 11, 2026
aee7976
refac
tjbck Jul 11, 2026
a18b1c2
refac
tjbck Jul 11, 2026
d947c5f
refac
tjbck Jul 11, 2026
4aab53a
refac
tjbck Jul 11, 2026
2196739
refac
tjbck Jul 11, 2026
28064c1
refac
tjbck Jul 11, 2026
9c47e65
refac
tjbck Jul 11, 2026
650fb25
refac
tjbck Jul 11, 2026
2d30dfe
refac
tjbck Jul 11, 2026
751c871
refac
tjbck Jul 11, 2026
f0c9751
refac
tjbck Jul 11, 2026
9e3d80a
refac
tjbck Jul 11, 2026
8e70ecf
refac
tjbck Jul 11, 2026
5f3ef71
refac
tjbck Jul 11, 2026
41cf2d3
refac
tjbck Jul 11, 2026
9554abf
refac
tjbck Jul 11, 2026
c944575
refac
tjbck Jul 11, 2026
d21bb55
reafc
tjbck Jul 11, 2026
2b8bcdc
refac
tjbck Jul 11, 2026
98334ef
refac
tjbck Jul 11, 2026
055c205
refac
tjbck Jul 11, 2026
5438ab2
refac
tjbck Jul 11, 2026
c4d55bf
refac
tjbck Jul 12, 2026
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.9.0] - 2026-07-11

### Added

- 🌐 **A browser in your workspace.** Open websites and local app previews in their own tabs, with back and forward navigation, mobile views, and display-quality choices.
- 🤖 **More coding agents.** Add Gemini or Pi alongside the coding tools you already use.
- 🔎 **Search inside files.** Find file names and matching text, then jump straight to the right line.
- 💬 **A more useful home screen.** Start a chat, terminal, or browser before opening a workspace, and arrange them side by side.

### Changed

- 🗂️ **Easier chat organization.** Rename chats, copy their saved location, and load more of a workspace's chats without leaving the sidebar.
- ✨ **More control over the view.** Choose a wider layout and decide whether tool details stay expanded.

## [0.8.8] - 2026-07-09

### Added
Expand Down
2 changes: 1 addition & 1 deletion MANIFESTO.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Manifesto

> **TL;DR** We pushed code and went to dinner. Prod broke. Phone in pocket, laptop at home. The fix was on our computer, not in some fresh cloud box. So we built `cptr`: your actual machine, in any browser. Files, terminal, editor, git, running sessions, all of it. Phone, tablet, laptop, whatever. It literally is your computer. Now go outside.
> **TL;DR** We pushed code and went to dinner. Prod broke. Phone in pocket, laptop at home. The fix was on our computer, not in some fresh cloud box. So we built Open WebUI Computer: your actual machine, in any browser. Files, terminal, editor, git, running sessions, all of it. Phone, tablet, laptop, whatever. It literally is your computer. Now go outside.

Your computer is where your work lives.

Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Open WebUI Computer (cptr)
# Open WebUI Computer

![GitHub stars](https://img.shields.io/github/stars/open-webui/computer?style=social)
![GitHub forks](https://img.shields.io/github/forks/open-webui/computer?style=social)
Expand All @@ -12,12 +12,12 @@

![Open WebUI Computer Demo](./demo.png)

<sub>See more at [cptr.sh](https://cptr.sh/)</sub>

Open WebUI Computer (`cptr`) runs on your machine and serves your whole computer to any browser: files, terminal, editor, git, running sessions, AI agents, and tools. It literally is your computer.

Use it from your phone, tablet, laptop, another computer, or the machine it's running on. Designed to feel native on every screen. Connect your own AI via API key, plug in a coding agent you already subscribe to, or work directly in the terminal. One tool, full workstation, any device.

> Start here: [Open WebUI Computer docs](https://docs.openwebui.com/ecosystem/computer/)

## Install

```bash
Expand Down Expand Up @@ -101,11 +101,11 @@ Bring your own API key (OpenAI, Anthropic, Ollama, or any OpenAI-compatible endp

Use the subscriptions you already pay for as native backends on your own machine. No separate API key needed.

**Codex** · **Claude Code** · **Cursor** · **Grok** · **OpenCode** · **Cline**
**Codex** · **Claude Code** · **Cursor** · **Grok** · **OpenCode** · **Cline** · **Gemini** · **Pi**

Add an agent profile from Settings, pick your models, and it shows up in the model selector like any other provider. Conversations run inside your workspace with full tool access and resume where you left off.

Prefer to run agents yourself? Any terminal agent (Gemini CLI, Kilo Code, Pi, and others) works in the terminal tab the way it always has.
Prefer to run agents yourself? Any terminal agent (Kilo Code, Pi, and others) works in the terminal tab the way it always has.

## Messaging bots

Expand Down
18 changes: 7 additions & 11 deletions cptr/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
auth_router,
automations_router,
bridge_router,
browser_router,
webhook_router,
chat_router,
events_router,
Expand All @@ -20,7 +21,6 @@
images_router,
memory_router,
notifications_router,
proxy_router,
search_router,
skills_router,
state_router,
Expand Down Expand Up @@ -84,11 +84,15 @@ async def shutdown():
await cancel_all_async_subagents(reason="shutdown")
except Exception:
pass
# Clean up browser sessions and launched Chrome
# Clean up browser sessions and launched Chrome used by agent tools.
try:
from cptr.utils.browser.session import session_manager
from cptr.utils.browser.launcher import shutdown_browser
from cptr.utils.browser.proxy import manager as browser_proxy_manager
from cptr.utils.browser.viewer import manager as chrome_viewer_manager

await chrome_viewer_manager.close_all()
await browser_proxy_manager.close_all()
await session_manager.close_all()
await shutdown_browser()
except Exception:
Expand Down Expand Up @@ -153,14 +157,6 @@ async def auth_middleware(request: Request, call_next):
)


# Proxy fallback middleware: intercepts sub-resource requests for proxied
# dev servers (JS modules, CSS, etc.) based on the Referer header chain.
# Added after auth middleware so it wraps outermost (runs before auth),
# since proxied sub-resources don't need cptr auth.
from cptr.utils.proxy_middleware import ProxyFallbackMiddleware

app.add_middleware(ProxyFallbackMiddleware)

# CORS middleware: uses CPTR_CORS_ALLOWED_ORIGINS env var (default "*").
from fastapi.middleware.cors import CORSMiddleware
from cptr.env import CORS_ALLOWED_ORIGINS
Expand Down Expand Up @@ -260,6 +256,7 @@ async def get_config():
app.include_router(auth_router)
app.include_router(automations_router)
app.include_router(bridge_router)
app.include_router(browser_router)
app.include_router(webhook_router)
app.include_router(chat_router)
app.include_router(events_router)
Expand All @@ -269,7 +266,6 @@ async def get_config():
app.include_router(images_router)
app.include_router(memory_router)
app.include_router(notifications_router)
app.include_router(proxy_router)
app.include_router(search_router)
app.include_router(skills_router)
app.include_router(state_router)
Expand Down
7 changes: 5 additions & 2 deletions cptr/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ def cli():
@click.option("--headless", is_flag=True, default=False, help="Don't open browser.")
def run(host: str, port: int, reload: bool, headless: bool):
"""Start the cptr server."""
import os, secrets
import os
import secrets

display_host = "localhost" if host == "0.0.0.0" else host

token = secrets.token_hex(32)
os.environ["CPTR_STARTUP_TOKEN"] = token
os.environ["CPTR_PORT"] = str(port)
url = f"http://{display_host}:{port}/?token={token}"

print(f"\n ➜ {url}\n")
if not headless:
import threading, webbrowser
import threading
import webbrowser

threading.Timer(1.5, lambda: webbrowser.open(url)).start()
uvicorn.run(
Expand Down
4 changes: 2 additions & 2 deletions cptr/frontend/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
--app-fg: #525252;
--app-fg-muted: color-mix(in oklab, var(--app-fg) 62%, var(--app-bg));
--app-fg-subtle: color-mix(in oklab, var(--app-fg) 48%, var(--app-bg));
--app-border: color-mix(in oklab, var(--app-fg) 10%, transparent);
--app-divider: color-mix(in oklab, var(--app-fg) 8%, transparent);
--app-border: color-mix(in oklab, var(--app-fg) 7%, transparent);
--app-divider: color-mix(in oklab, var(--app-fg) 5%, transparent);
--app-hover: color-mix(in oklab, var(--app-fg) 6%, transparent);
--app-active: color-mix(in oklab, var(--app-fg) 7%, transparent);
--app-ui-font: var(--font-sans);
Expand Down
10 changes: 9 additions & 1 deletion cptr/frontend/src/lib/apis/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,15 @@ export const updateConfig = (config: Record<string, unknown>) =>

// ── Agents ─────────────────────────────────────────────────

export type AgentType = 'codex' | 'claude_code' | 'cursor' | 'grok' | 'opencode' | 'cline';
export type AgentType =
| 'codex'
| 'claude_code'
| 'cursor'
| 'grok'
| 'opencode'
| 'cline'
| 'gemini'
| 'pi';
export type AgentMode = 'auto' | 'enabled' | 'disabled';
export type AgentStatus = 'ready' | 'not_found' | 'missing_dependency' | 'auth_unknown' | 'error';

Expand Down
91 changes: 91 additions & 0 deletions cptr/frontend/src/lib/apis/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { fetchHandler, fetchJSON } from '$lib/apis';

export interface BrowserSession {
session_id: string;
url: string;
title: string;
mode: 'proxy' | 'chrome';
status: string;
}

export interface BrowserAvailability {
proxy: { available: true };
chrome: {
available: boolean;
reason: string | null;
};
}

export interface PersonalChromeStatus {
status: 'disconnected' | 'connecting' | 'playing' | 'lost' | 'unavailable';
browser: string;
session_count: number;
}

let availabilityPromise: Promise<BrowserAvailability> | undefined;
export const getBrowserAvailability = () =>
(availabilityPromise ??= fetchJSON<BrowserAvailability>('/api/browser/availability'));

export const createBrowserSession = (url?: string) =>
fetchJSON<BrowserSession>('/api/browser/sessions', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ url })
});

export const listBrowserSessions = async () => {
const data = await fetchJSON<{ session_ids: string[] }>('/api/browser/sessions');
return data.session_ids;
};

export const deleteBrowserSession = (sessionId: string) =>
fetchHandler(`/api/browser/sessions/${sessionId}`, { method: 'DELETE' }).catch(() => {});

export const getBrowserSession = (sessionId: string) =>
fetchJSON<BrowserSession>(`/api/browser/sessions/${sessionId}`);

export const updateBrowserSession = (sessionId: string, url: string, title: string) =>
fetchJSON<BrowserSession>(`/api/browser/sessions/${sessionId}`, {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ url, title })
});

export const setBrowserMode = (sessionId: string, mode: 'proxy' | 'chrome', url: string) =>
fetchJSON<BrowserSession>(`/api/browser/sessions/${sessionId}`, {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ mode, url })
});

export const browserFrameUrl = (sessionId: string, rawUrl: string) => {
const normalized = /^https?:\/\//i.test(rawUrl) ? rawUrl : `https://${rawUrl}`;
const url = new URL(normalized);
return `/api/browser/frame/${sessionId}/${url.protocol.slice(0, -1)}/${encodeURIComponent(url.host)}${url.pathname}${url.search}${url.hash}`;
};

export const browserBlankUrl = (sessionId: string) => `/api/browser/sessions/${sessionId}/blank`;

export const clearManagedChromeProfile = () =>
fetchJSON<{ status: string; closed_session_ids: string[] }>('/api/browser/profile', {
method: 'DELETE'
});

export const testBrowserCdp = (url: string) =>
fetchJSON<{ browser: string }>('/api/browser/cdp', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ url })
});

export const getPersonalChrome = () => fetchJSON<PersonalChromeStatus>('/api/browser/personal');

export const connectPersonalChrome = (url: string) =>
fetchJSON<PersonalChromeStatus>('/api/browser/personal', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ url })
});

export const disconnectPersonalChrome = () =>
fetchJSON<PersonalChromeStatus>('/api/browser/personal', { method: 'DELETE' });
15 changes: 11 additions & 4 deletions cptr/frontend/src/lib/apis/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@ export interface CompactChatResult {
// ── Queries ─────────────────────────────────────────────────

export const getChats = (
workspace: string,
workspace?: string,
limit = 50,
offset = 0,
sortBy: 'title' | 'updated_at' = 'updated_at',
sortDir: 'asc' | 'desc' = 'desc'
) =>
fetchJSON<{ chats: ChatInfo[]; total: number; has_more: boolean }>(
`/api/chats?workspace=${encodeURIComponent(workspace)}&limit=${limit}&offset=${offset}&sort_by=${sortBy}&sort_dir=${sortDir}`
`/api/chats?${workspace ? `workspace=${encodeURIComponent(workspace)}&` : ''}limit=${limit}&offset=${offset}&sort_by=${sortBy}&sort_dir=${sortDir}`
);

export const getChat = (chatId: string, modelId?: string) => {
Expand All @@ -97,6 +97,13 @@ export const getChat = (chatId: string, modelId?: string) => {
export const deleteChat = (chatId: string) =>
fetchJSON<{ ok: boolean }>(`/api/chats/${chatId}`, { method: 'DELETE' });

export const updateChatTitle = (chatId: string, title: string) =>
fetchJSON<{ ok: boolean; title: string }>(`/api/chats/${chatId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title })
});

export const forkChat = (chatId: string, messageId?: string | null) =>
fetchJSON<{ ok: boolean; chat_id: string }>(
`/api/chats/${chatId}/fork`,
Expand All @@ -108,7 +115,7 @@ export const forkChat = (chatId: string, messageId?: string | null) =>
export const sendMessage = (
content: string,
modelId: string,
workspace: string,
workspace?: string,
chatId?: string,
parentId?: string | null,
params: ChatSendParams = {},
Expand All @@ -120,7 +127,7 @@ export const sendMessage = (
jsonBody({
content,
model_id: modelId,
workspace,
...(workspace ? { workspace } : {}),
chat_id: chatId,
parent_id: parentId ?? null,
regeneration_prompt: regenerationPrompt,
Expand Down
23 changes: 23 additions & 0 deletions cptr/frontend/src/lib/apis/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,26 @@ export const searchFiles = (query: string, path: string) =>
fetchJSON(
`/api/workspace/files/search?query=${encodeURIComponent(query)}&path=${encodeURIComponent(path)}`
);

export type ContentMatch = { line: number; column: number; text: string };
export type FileMatch = {
path: string;
relative_path: string;
name: string;
type: 'file' | 'directory';
name_match: boolean;
content_matches: ContentMatch[];
};
export type FileMatches = { results: FileMatch[]; next_offset: number | null };

export const getFileMatches = (
query: string,
path: string,
showHidden: boolean,
offset = 0,
signal?: AbortSignal
) =>
fetchJSON<FileMatches>(
`/api/workspace/files/matches?query=${encodeURIComponent(query)}&path=${encodeURIComponent(path)}&show_hidden=${showHidden}&offset=${offset}`,
{ signal }
);
7 changes: 5 additions & 2 deletions cptr/frontend/src/lib/apis/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,8 @@ export const unifiedSearch = (
return fetchJSON<UnifiedSearchResponse>(`/api/search?${params}`);
};

export const getRecentChats = (limit = 9) =>
fetchJSON<{ chats: ChatSearchResult[] }>(`/api/search/recent?limit=${limit}`);
export const getRecentChats = (limit = 9, workspace?: string) => {
const params = new URLSearchParams({ limit: String(limit) });
if (workspace) params.set('workspace', workspace);
return fetchJSON<{ chats: ChatSearchResult[] }>(`/api/search/recent?${params}`);
};
6 changes: 4 additions & 2 deletions cptr/frontend/src/lib/apis/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ export interface SkillInfo {
last_updated_at?: string | null;
}

export const getSkills = (workspace: string) =>
fetchJSON<SkillInfo[]>(`/api/skills?workspace=${encodeURIComponent(workspace)}`);
export const getSkills = (workspace?: string) =>
fetchJSON<SkillInfo[]>(
workspace ? `/api/skills?workspace=${encodeURIComponent(workspace)}` : '/api/skills'
);
4 changes: 2 additions & 2 deletions cptr/frontend/src/lib/apis/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export interface CommandSession {

export const listSessions = () => fetchJSON<TerminalSession[]>('/api/terminal');

export const createSession = (cwd: string) =>
fetchJSON<TerminalSession>('/api/terminal', jsonBody({ cwd }));
export const createSession = (cwd?: string) =>
fetchJSON<TerminalSession>('/api/terminal', jsonBody(cwd ? { cwd } : {}));

export const deleteSession = (sessionId: string) =>
fetchHandler(`/api/terminal/${sessionId}`, { method: 'DELETE' }).catch(() => {});
Expand Down
Loading
Loading