Skip to content
Merged

refac #132

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
71 changes: 56 additions & 15 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,23 @@ jobs:
- platform: linux/amd64
runner: ubuntu-latest
arch: amd64
target: default
flavor: default
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
target: default
flavor: default
- platform: linux/amd64
runner: ubuntu-latest
arch: amd64
target: browser
flavor: browser
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
target: browser
flavor: browser
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout
Expand All @@ -70,10 +84,11 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
target: ${{ matrix.target }}
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=dev-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=dev-${{ matrix.arch }}
cache-from: type=gha,scope=dev-${{ matrix.flavor }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=dev-${{ matrix.flavor }}-${{ matrix.arch }}

- name: Export digest
run: |
Expand All @@ -84,21 +99,24 @@ jobs:
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-dev-${{ matrix.arch }}
name: digests-dev-${{ matrix.flavor }}-${{ matrix.arch }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

merge-dev:
if: github.ref == 'refs/heads/main'
needs: build-dev
strategy:
matrix:
flavor: [default, browser]
runs-on: ubuntu-latest
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-dev-*
pattern: digests-dev-${{ matrix.flavor }}-*
merge-multiple: true

- name: Set up Docker Buildx
Expand All @@ -115,9 +133,12 @@ jobs:
working-directory: /tmp/digests
run: |
SHORT_SHA="${GITHUB_SHA::7}"
docker buildx imagetools create \
-t "${{ env.IMAGE }}:dev" \
-t "${{ env.IMAGE }}:dev-${SHORT_SHA}" \
if [ "${{ matrix.flavor }}" = default ]; then
tags=("${{ env.IMAGE }}:dev" "${{ env.IMAGE }}:dev-${SHORT_SHA}")
else
tags=("${{ env.IMAGE }}:browser-dev" "${{ env.IMAGE }}:browser-dev-${SHORT_SHA}")
fi
docker buildx imagetools create $(printf -- '-t %s ' "${tags[@]}") \
$(printf '${{ env.IMAGE }}@sha256:%s ' *)

# ── Release builds (release branch) ───────────────────────────────────────
Expand All @@ -131,9 +152,23 @@ jobs:
- platform: linux/amd64
runner: ubuntu-latest
arch: amd64
target: default
flavor: default
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
target: default
flavor: default
- platform: linux/amd64
runner: ubuntu-latest
arch: amd64
target: browser
flavor: browser
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
target: browser
flavor: browser
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout
Expand All @@ -154,10 +189,11 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
target: ${{ matrix.target }}
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=release-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=release-${{ matrix.arch }}
cache-from: type=gha,scope=release-${{ matrix.flavor }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=release-${{ matrix.flavor }}-${{ matrix.arch }}

- name: Export digest
run: |
Expand All @@ -168,21 +204,24 @@ jobs:
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-release-${{ matrix.arch }}
name: digests-release-${{ matrix.flavor }}-${{ matrix.arch }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

merge-release:
needs: [read-version, build-release]
if: github.ref == 'refs/heads/release'
strategy:
matrix:
flavor: [default, browser]
runs-on: ubuntu-latest
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-release-*
pattern: digests-release-${{ matrix.flavor }}-*
merge-multiple: true

- name: Set up Docker Buildx
Expand All @@ -200,8 +239,10 @@ jobs:
run: |
VERSION="${{ needs.read-version.outputs.version }}"
MAJOR_MINOR="${VERSION%.*}"
docker buildx imagetools create \
-t "${{ env.IMAGE }}:latest" \
-t "${{ env.IMAGE }}:${VERSION}" \
-t "${{ env.IMAGE }}:${MAJOR_MINOR}" \
if [ "${{ matrix.flavor }}" = default ]; then
tags=("${{ env.IMAGE }}:latest" "${{ env.IMAGE }}:${VERSION}" "${{ env.IMAGE }}:${MAJOR_MINOR}")
else
tags=("${{ env.IMAGE }}:browser" "${{ env.IMAGE }}:${VERSION}-browser" "${{ env.IMAGE }}:${MAJOR_MINOR}-browser")
fi
docker buildx imagetools create $(printf -- '-t %s ' "${tags[@]}") \
$(printf '${{ env.IMAGE }}@sha256:%s ' *)
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +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.5] - 2026-07-11

### Added

- 👀 **Never lose track of a finished chat.** Unread markers now appear in tabs and chat lists, with a count beside each workspace.
- 🌐 **Browser tasks in Docker.** A browser-ready Docker option is now available when your chats need to use the web.

## [0.9.4] - 2026-07-11

### Fixed

- ⚡ **Your model choices are ready sooner.** Computer now gets them ready as it opens and keeps the last working list if a check fails.
- 👋 **More dependable restarts.** Computer now finishes its open work more reliably before it closes.

## [0.9.3] - 2026-07-11

### Changed
Expand Down
22 changes: 18 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ COPY --from=frontend-builder /build/frontend/build cptr/frontend/build
RUN uv build --wheel --out-dir /dist


# ── Stage 3: Minimal runtime image ─────────────────────────
# ── Stage 3: Shared runtime ────────────────────────────────
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS runtime

LABEL org.opencontainers.image.source="https://github.com/open-webui/computer"
LABEL org.opencontainers.image.description="cptr: your computer, from anywhere"
LABEL org.opencontainers.image.description="Open WebUI Computer"

# Runtime deps: git for git operations, tini for PID 1
# Runtime deps shared by default and browser images.
RUN apt-get update && \
apt-get install -y --no-install-recommends git tini && \
apt-get install -y --no-install-recommends gh git tini && \
rm -rf /var/lib/apt/lists/*

# Create non-root user and writable data directory
Expand All @@ -55,3 +55,17 @@ VOLUME ["/data"]

ENTRYPOINT ["tini", "--"]
CMD ["cptr", "run", "--host", "0.0.0.0", "--port", "8000", "--headless"]


# ── Browser image: Chromium for agent browser automation ───
FROM runtime AS browser

USER root
RUN apt-get update && \
apt-get install -y --no-install-recommends chromium && \
rm -rf /var/lib/apt/lists/*
USER cptr


# ── Default image ──────────────────────────────────────────
FROM runtime AS default
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ docker run --rm -it \
Then open the URL printed in the logs, usually `http://localhost:8000/?token=...`.

Open WebUI Computer stores its state in `/data`. Mount your project into the container, like `-v "$PWD:/workspace"`, so Open WebUI Computer can access it.
The default image includes Git and GitHub CLI (`gh`). Use `ghcr.io/open-webui/computer:browser` when agent browser automation needs Chromium.

If you bind-mount a host directory to `/data`, make sure that directory is writable by the container user. SQLite needs to create and update `/data/app.db`, and host directory permissions take precedence over the image's built-in `/data` ownership.

Expand Down
1 change: 1 addition & 0 deletions cptr/frontend/src/lib/apis/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface ChatInfo {
current_message_id: string | null;
created_at: number;
updated_at: number;
last_read_at: number | null;
is_active?: boolean;
}

Expand Down
9 changes: 7 additions & 2 deletions cptr/frontend/src/lib/apis/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ export const savePreferences = (data: Record<string, unknown>) =>

// ── Workspace list (sidebar) ────────────────────────────────────

export const getWorkspaceList = () =>
fetchJSON<{ path: string; name: string }[]>('/api/state/workspaces');
export interface WorkspaceListItem {
path: string;
name: string;
unread_count: number;
}

export const getWorkspaceList = () => fetchJSON<WorkspaceListItem[]>('/api/state/workspaces');

// ── Single workspace CRUD ───────────────────────────────────────

Expand Down
9 changes: 7 additions & 2 deletions cptr/frontend/src/lib/components/GroupTabBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
type Tab
} from '$lib/stores';
import { openChatTab } from '$lib/stores';
import { chatEnabled, streamingChatTabs } from '$lib/stores/chat';
import { chatEnabled, chatStatuses, isChatUnread, streamingChatTabs } from '$lib/stores/chat';
import { voiceMemosEnabled, showVoiceMemo } from '$lib/stores/audio';
import { keybindings, formatChord } from '$lib/stores/keybindings';
import Icon from './Icon.svelte';
Expand Down Expand Up @@ -407,6 +407,8 @@
<div bind:this={tabsEl} class="flex items-center gap-0.5 shrink-0">
{#each displayTabs as tab (tab.id)}
{@const isActive = tab.id === group.activeTabId}
{@const chatStatus =
tab.type === 'chat' && tab.path ? $chatStatuses.get(tab.path) : undefined}
<button
class="flex items-center gap-1.5 px-2.5 h-7 rounded-lg text-xs font-medium whitespace-nowrap shrink-0 transition-all duration-100
{isActive
Expand All @@ -417,14 +419,17 @@
onclick={() => handleTabClick(tab)}
oncontextmenu={(e) => handleContextMenu(e, tab)}
>
{#if tab.type === 'chat' && $streamingChatTabs.has(tab.id)}
{#if tab.type === 'chat' && (chatStatus?.active || $streamingChatTabs.has(tab.id))}
<Spinner size={14} />
{:else}
<Icon name={tabIconName(tab)} size={14} />
{/if}
<span class="max-w-30 overflow-hidden text-ellipsis">
{tab.type === 'files' ? ($activeWorkspace?.name ?? $t('bar.files')) : tab.label}
</span>
{#if tab.type === 'chat' && !isActive && isChatUnread(chatStatus)}
<span class="size-1.5 shrink-0 rounded-full bg-sky-500" aria-hidden="true"></span>
{/if}
{#if tab.unsaved}<span class="w-1.5 h-1.5 rounded-full bg-gray-400 shrink-0"></span>{/if}
{#if !tab.permanent}
<!-- svelte-ignore a11y_no_static_element_interactions -->
Expand Down
Loading
Loading