diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2ad3a953..4246e204 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -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 @@ -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: | @@ -84,7 +99,7 @@ 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 @@ -92,13 +107,16 @@ jobs: 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 @@ -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) ─────────────────────────────────────── @@ -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 @@ -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: | @@ -168,7 +204,7 @@ 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 @@ -176,13 +212,16 @@ jobs: 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 @@ -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 ' *) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a8e0cf..d8b93594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Dockerfile b/Dockerfile index 25762cc5..90ada636 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 @@ -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 diff --git a/README.md b/README.md index 01166942..9a7302db 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cptr/frontend/src/lib/apis/chat.ts b/cptr/frontend/src/lib/apis/chat.ts index 464513ce..a5c8339d 100644 --- a/cptr/frontend/src/lib/apis/chat.ts +++ b/cptr/frontend/src/lib/apis/chat.ts @@ -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; } diff --git a/cptr/frontend/src/lib/apis/state.ts b/cptr/frontend/src/lib/apis/state.ts index ee49b37a..10d64b80 100644 --- a/cptr/frontend/src/lib/apis/state.ts +++ b/cptr/frontend/src/lib/apis/state.ts @@ -17,8 +17,13 @@ export const savePreferences = (data: Record) => // ── 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('/api/state/workspaces'); // ── Single workspace CRUD ─────────────────────────────────────── diff --git a/cptr/frontend/src/lib/components/GroupTabBar.svelte b/cptr/frontend/src/lib/components/GroupTabBar.svelte index 3a8933ee..587ad1f6 100644 --- a/cptr/frontend/src/lib/components/GroupTabBar.svelte +++ b/cptr/frontend/src/lib/components/GroupTabBar.svelte @@ -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'; @@ -407,6 +407,8 @@
{#each displayTabs as tab (tab.id)} {@const isActive = tab.id === group.activeTabId} + {@const chatStatus = + tab.type === 'chat' && tab.path ? $chatStatuses.get(tab.path) : undefined}