fix: move to npm
perso/opencode-openchamber/pipeline/head Build queued...

This commit is contained in:
Julien Cabillot
2026-04-27 16:47:29 -04:00
parent 447ec8ab99
commit e19c8c4924
2183 changed files with 25 additions and 339607 deletions
+24
View File
@@ -0,0 +1,24 @@
FROM jcabillot/opencode:latest
ARG OPENCHAMBER_WEB_VERSION=1.9.9
ENV NPM_CONFIG_UPDATE_NOTIFIER=false \
NPM_CONFIG_LOGLEVEL=warn \
NODE_ENV=production
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/* \
&& npm install -g --no-fund --no-audit "@openchamber/web@${OPENCHAMBER_WEB_VERSION}" \
&& openchamber --version \
&& chown -R opencode:opencode /home/opencode
WORKDIR /home/opencode/
USER opencode
EXPOSE 3000
ENTRYPOINT ["openchamber"]
CMD ["serve", "--host", "0.0.0.0", "--port", "3000", "--foreground"]
Vendored
+1 -1
View File
@@ -20,7 +20,7 @@ pipeline {
stage('Build image') {
steps{
sh 'docker build --force-rm=true --no-cache=true --pull -f src/Dockerfile -t ${dockerImage} src/'
sh 'docker build --force-rm=true --no-cache=true --pull -f Dockerfile -t ${dockerImage} .'
}
}
-26
View File
@@ -1,26 +0,0 @@
.git
.gitignore
node_modules
**/node_modules
dist
**/dist
build
**/build
.DS_Store
.idea
.vscode
coverage
tmp
logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.env
.env.*
.opencode
data
workspaces
packages/desktop/src-tauri
packages/desktop/target
packages/intellij
-2
View File
@@ -1,2 +0,0 @@
# .github/CODEOWNERS
* @btriapitsyn
-45
View File
@@ -1,45 +0,0 @@
name: Bug report
description: Report something broken
title: "[Bug] "
labels: [bug]
body:
- type: textarea
id: what
attributes:
label: What's wrong? Maybe some steps to reproduce.
description: What happened and what you expected.
placeholder: "Expected … but got …"
validations:
required: true
- type: dropdown
id: runtime
attributes:
label: Where does it happen?
options:
- Desktop (macOS)
- Desktop Web
- Mobile (Web/PWA)
- VS Code extension
- Not sure
validations:
required: true
- type: input
id: version
attributes:
label: Version (if known)
placeholder: "e.g. 1.2.3"
- type: textarea
id: screenshots
attributes:
label: Screenshots / recordings (optional)
description: "Anything visual that helps."
- type: textarea
id: logs
attributes:
label: Logs (optional)
description: "Paste relevant logs."
render: shell
-1
View File
@@ -1 +0,0 @@
blank_issues_enabled: false
-22
View File
@@ -1,22 +0,0 @@
name: Feature request
description: Suggest an improvement
title: "[Feature Request] "
labels: [enhancement]
body:
- type: textarea
id: request
attributes:
label: What should we add/change?
description: What you're trying to do + what you'd like to happen.
placeholder: |
I'm trying to …
It would be great if OpenChamber …
validations:
required: true
- type: textarea
id: context
attributes:
label: Extra context (optional)
description: Links, screenshots, mockups, constraints, etc.
-201
View File
@@ -1,201 +0,0 @@
name: Build macOS DMG (arm64)
on:
workflow_dispatch:
inputs:
macos_version:
description: macOS runner version
required: true
type: choice
options:
- "macos-15"
- "macos-26"
default: "macos-15"
ref:
description: Git ref to build (branch, tag, or sha)
required: false
default: ""
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
jobs:
build-macos-dmg-arm64:
name: Build DMG (arm64, ${{ inputs.macos_version }})
runs-on: ${{ inputs.macos_version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: packages/desktop/src-tauri
key: aarch64-apple-darwin
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Install Apple Certificate
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 \
-P "$APPLE_CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 \
-k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
- name: Set up notarization credentials
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
run: |
if [ -z "$APPLE_ID" ] || [ -z "$APPLE_TEAM_ID" ] || [ -z "$APPLE_PASSWORD" ]; then
echo "Error: Missing Apple notarization credentials"
exit 1
fi
xcrun notarytool store-credentials "openchamber-notarize" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_PASSWORD"
- name: Build UI package
run: bun run --cwd packages/ui build
- name: Build Desktop app (arm64)
run: bun run --cwd packages/desktop build && bun run --cwd packages/desktop tauri build --target aarch64-apple-darwin
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Prepare DMG artifact
run: |
set -euo pipefail
mkdir -p artifacts
DMG_PATH="packages/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/*.dmg"
if ls $DMG_PATH 1> /dev/null 2>&1; then
DMG_FILE=$(ls $DMG_PATH | head -n 1)
DMG_NAME="OpenChamber_${{ inputs.macos_version }}_arm64.dmg"
cp "$DMG_FILE" "artifacts/$DMG_NAME"
else
echo "Error: DMG file not found at $DMG_PATH"
exit 1
fi
- name: Upload DMG artifact
uses: actions/upload-artifact@v4
with:
name: dmg-${{ inputs.macos_version }}-arm64
path: artifacts/*.dmg
retention-days: 7
build-macos-dmg-arm64-electron:
name: Build Electron DMG (arm64, ${{ inputs.macos_version }})
runs-on: ${{ inputs.macos_version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Install Apple Certificate
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
KEYCHAIN_PATH=$RUNNER_TEMP/electron-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 \
-P "$APPLE_CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 \
-k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
- name: Build Electron app (arm64)
working-directory: packages/electron
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
ELECTRON_BUILDER_ARCH: arm64
run: |
bun run build:web-assets
bun run bundle:main
bun run rebuild:native
./node_modules/.bin/electron-builder --mac --arm64 --publish=never
- name: Prepare DMG artifact
run: |
set -euo pipefail
mkdir -p artifacts
DMG_PATH="packages/electron/dist/*.dmg"
if ls $DMG_PATH 1> /dev/null 2>&1; then
DMG_FILE=$(ls $DMG_PATH | head -n 1)
DMG_NAME="OpenChamber_Electron_${{ inputs.macos_version }}_arm64.dmg"
cp "$DMG_FILE" "artifacts/$DMG_NAME"
else
echo "Error: DMG file not found at $DMG_PATH"
exit 1
fi
- name: Upload DMG artifact
uses: actions/upload-artifact@v4
with:
name: dmg-electron-${{ inputs.macos_version }}-arm64
path: artifacts/*.dmg
retention-days: 7
-86
View File
@@ -1,86 +0,0 @@
name: Docs Source
on:
push:
branches: [main]
paths:
- "packages/docs/**"
- "scripts/docs/**"
- "package.json"
release:
types: [published]
workflow_dispatch:
inputs:
release_tag:
description: "Optional existing tag to upload docs source archive"
required: false
type: string
permissions:
contents: write
jobs:
validate-and-package:
runs-on: ubuntu-latest
outputs:
archive_name: ${{ steps.archive.outputs.archive_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Validate docs source
run: bun run docs:validate
- name: Build docs source archive
id: archive
run: |
mkdir -p artifacts
ARCHIVE_NAME="openchamber-docs-source-${GITHUB_SHA::8}.tar.gz"
tar -czf "artifacts/${ARCHIVE_NAME}" -C packages/docs .
echo "archive_name=${ARCHIVE_NAME}" >> "$GITHUB_OUTPUT"
- name: Upload workflow artifact
uses: actions/upload-artifact@v4
with:
name: docs-source
path: artifacts/${{ steps.archive.outputs.archive_name }}
retention-days: 14
- name: Upload archive to release tag
if: ${{ github.event_name == 'release' || github.event.inputs.release_tag != '' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag }}
files: artifacts/${{ steps.archive.outputs.archive_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger openchamber-website docs sync (optional)
if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
env:
WEBSITE_REPO: openchamber/openchamber-website
WEBSITE_TOKEN: ${{ secrets.OPENCHAMBER_WEBSITE_REPO_TOKEN }}
SOURCE_REF: ${{ github.event_name == 'release' && github.event.release.tag_name || github.ref_name }}
run: |
if [ -z "$WEBSITE_TOKEN" ]; then
echo "OPENCHAMBER_WEBSITE_REPO_TOKEN not set; skip dispatch."
exit 0
fi
curl -sS -X POST \
-H "Authorization: Bearer $WEBSITE_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/$WEBSITE_REPO/dispatches \
-d @- <<JSON
{
"event_type": "docs_source_updated",
"client_payload": {
"source_repo": "${{ github.repository }}",
"source_ref": "$SOURCE_REF",
"archive_name": "${{ steps.archive.outputs.archive_name }}"
}
}
JSON
-33
View File
@@ -1,33 +0,0 @@
name: oc integration
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
opencode:
if: |
contains(github.event.comment.body, ' /oc') ||
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Run opencode
uses: anomalyco/opencode/github@latest
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:
model: opencode/gpt-5.2-codex
-33
View File
@@ -1,33 +0,0 @@
name: pr checks
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
jobs:
checks:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build
run: bun run build
- name: Type check
run: bun run type-check
- name: Lint
run: bun run lint
-765
View File
@@ -1,765 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 0.1.0)'
required: true
type: string
dry_run:
description: 'Dry run (skip publishing)'
required: false
default: false
type: boolean
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
permissions:
contents: write
jobs:
create-release:
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create_release.outputs.id }}
release_upload_url: ${{ steps.create_release.outputs.upload_url }}
version: ${{ steps.get_version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Get version
id: get_version
run: |
if [[ -n "${{ github.event.inputs.version }}" ]]; then
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
else
echo "version=0.0.0-dev" >> $GITHUB_OUTPUT
fi
- name: Extract changelog for release
env:
VERSION: ${{ steps.get_version.outputs.version }}
run: |
node - <<'NODE'
const fs = require('fs');
const version = process.env.VERSION;
const changelogPath = 'CHANGELOG.md';
if (!fs.existsSync(changelogPath)) {
throw new Error('CHANGELOG.md not found; add it before releasing.');
}
const changelog = fs.readFileSync(changelogPath, 'utf8');
const sections = changelog.split(/^## /m);
const section = sections.find(s => s.startsWith('[' + version + ']'));
if (!section) {
throw new Error('Changelog section [' + version + '] not found. Add a section like "## [' + version + '] - YYYY-MM-DD".');
}
const content = ('## ' + section).trim();
fs.mkdirSync('artifacts', { recursive: true });
fs.writeFileSync('artifacts/release-notes.md', content + '\n');
NODE
- name: Create GitHub Release
id: create_release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.get_version.outputs.version }}
draft: true
generate_release_notes: false
body_path: artifacts/release-notes.md
name: OpenChamber v${{ steps.get_version.outputs.version }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-desktop-macos:
needs: create-release
runs-on: macos-26
strategy:
fail-fast: false
matrix:
target: [aarch64-apple-darwin, x86_64-apple-darwin]
include:
- target: aarch64-apple-darwin
arch: aarch64
platform: darwin-aarch64
- target: x86_64-apple-darwin
arch: x86_64
platform: darwin-x86_64
outputs:
version: ${{ needs.create-release.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: packages/desktop/src-tauri
key: ${{ matrix.target }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Install Apple Certificate
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
# Create temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Import certificate
echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 \
-P "$APPLE_CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 \
-k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
- name: Set up notarization credentials
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
run: |
# Validate secrets are set
if [ -z "$APPLE_ID" ] || [ -z "$APPLE_TEAM_ID" ] || [ -z "$APPLE_PASSWORD" ]; then
echo "Error: Missing Apple notarization credentials"
exit 1
fi
xcrun notarytool store-credentials "openchamber-notarize" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_PASSWORD"
- name: Build UI package
run: bun run --cwd packages/ui build
- name: Build Desktop app
# Note: We use inline commands instead of desktop:build to pass architecture-specific --target flag
# This enables cross-compilation for both arm64 and x86_64 from the same runner
run: |
export TAURI_ENV_TARGET_TRIPLE=${{ matrix.target }}
bun run --cwd packages/desktop build
bun run --cwd packages/desktop tauri build --target ${{ matrix.target }}
env:
TAURI_ENV_TARGET_TRIPLE: ${{ matrix.target }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Verify binary architectures
run: |
set -euo pipefail
BUNDLE_DIR="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos"
if [ ! -d "$BUNDLE_DIR" ]; then
echo "❌ Error: bundle directory not found: $BUNDLE_DIR"
exit 1
fi
APP_PATH=$(find "$BUNDLE_DIR" -maxdepth 2 -name "*.app" -print -quit)
if [ -z "$APP_PATH" ]; then
echo "❌ Error: .app bundle not found under $BUNDLE_DIR"
exit 1
fi
echo "🔍 Verifying binary architectures in $APP_PATH"
# Extract raw architecture names (macOS file command reports ARM as "arm64")
MAIN_ARCH_RAW=$(file "$APP_PATH/Contents/MacOS/openchamber-desktop" | grep -oE 'arm64|x86_64|aarch64' | head -1)
SIDEARCH_ARCH_RAW=$(file "$APP_PATH/Contents/MacOS/openchamber-server" | grep -oE 'arm64|x86_64|aarch64' | head -1)
# Normalize architecture names (arm64 -> aarch64 for consistency with Rust/Tauri)
normalize_arch() {
case "$1" in
arm64) echo "aarch64" ;;
aarch64|x86_64) echo "$1" ;;
*) echo "unknown" ;;
esac
}
MAIN_ARCH=$(normalize_arch "$MAIN_ARCH_RAW")
SIDEARCH_ARCH=$(normalize_arch "$SIDEARCH_ARCH_RAW")
EXPECTED_ARCH=$(echo "${{ matrix.target }}" | grep -oE 'aarch64|x86_64' | head -1)
echo " Main: $MAIN_ARCH_RAW → $MAIN_ARCH"
echo " Sidecar: $SIDEARCH_ARCH_RAW → $SIDEARCH_ARCH"
echo " Expected: $EXPECTED_ARCH"
if [ "$MAIN_ARCH" != "$EXPECTED_ARCH" ]; then
echo "❌ ERROR: Main binary architecture mismatch!"
echo " Expected: $EXPECTED_ARCH"
echo " Got: $MAIN_ARCH (raw: $MAIN_ARCH_RAW)"
exit 1
fi
if [ "$SIDEARCH_ARCH" != "$EXPECTED_ARCH" ]; then
echo "❌ ERROR: Sidecar binary architecture mismatch!"
echo " Expected: $EXPECTED_ARCH"
echo " Got: $SIDEARCH_ARCH (raw: $SIDEARCH_ARCH_RAW)"
exit 1
fi
echo "✅ Architecture verification passed: both binaries match $EXPECTED_ARCH"
- name: Verify macOS entitlements
run: |
set -euo pipefail
BUNDLE_DIR="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos"
if [ ! -d "$BUNDLE_DIR" ]; then
echo "Error: bundle directory not found: $BUNDLE_DIR"
exit 1
fi
APP_PATH=$(find "$BUNDLE_DIR" -maxdepth 2 -name "*.app" -print -quit)
if [ -z "$APP_PATH" ]; then
echo "Error: .app bundle not found under $BUNDLE_DIR"
echo "Contents:"; ls -la "$BUNDLE_DIR"
exit 1
fi
echo "Verifying app bundle: $APP_PATH"
codesign -vv "$APP_PATH"
ENTITLEMENTS=$(codesign -d --entitlements :- "$APP_PATH" 2>&1 || true)
echo "$ENTITLEMENTS"
if echo "$ENTITLEMENTS" | grep -q "com.apple.security.app-sandbox"; then
echo "Error: app sandbox entitlement is present"
exit 1
fi
for key in \
com.apple.security.cs.allow-jit \
com.apple.security.cs.allow-unsigned-executable-memory \
com.apple.security.cs.disable-executable-page-protection \
com.apple.security.cs.disable-library-validation
do
if ! echo "$ENTITLEMENTS" | grep -q "<key>$key</key>"; then
echo "Error: required entitlement missing: $key"
exit 1
fi
done
- name: Prepare release artifacts
run: |
mkdir -p artifacts
VERSION="${{ needs.create-release.outputs.version }}"
# Copy DMG (Tauri names it with the target triple in the path)
DMG_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg"
if ls $DMG_PATH 1> /dev/null 2>&1; then
DMG_FILE=$(ls $DMG_PATH | head -n 1)
DMG_NAME="OpenChamber_${VERSION}_${{ matrix.platform }}.dmg"
cp "$DMG_FILE" "artifacts/$DMG_NAME"
else
echo "Error: DMG file not found at $DMG_PATH"
exit 1
fi
# Copy tar.gz and signature for updater
TAR_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz"
SIG_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz.sig"
if ls $TAR_PATH 1> /dev/null 2>&1; then
TAR_FILE=$(ls $TAR_PATH | head -n 1)
TAR_BASE=$(basename "$TAR_FILE")
TAR_NAME="${TAR_BASE%.tar.gz}-${{ matrix.platform }}.tar.gz"
cp "$TAR_FILE" "artifacts/$TAR_NAME"
else
echo "Error: tar.gz file not found at $TAR_PATH"
exit 1
fi
if ls $SIG_PATH 1> /dev/null 2>&1; then
SIG_FILE=$(ls $SIG_PATH | head -n 1)
SIG_BASE=$(basename "$SIG_FILE")
SIG_NAME="${SIG_BASE%.tar.gz.sig}-${{ matrix.platform }}.tar.gz.sig"
cp "$SIG_FILE" "artifacts/$SIG_NAME"
else
echo "Error: signature file not found at $SIG_PATH"
exit 1
fi
echo "Successfully prepared artifacts:"
ls -lh artifacts/
- name: Generate update manifest
run: |
VERSION="${{ needs.create-release.outputs.version }}"
# Find the signature file for this platform
SIG_FILE=$(find artifacts -name "*-${{ matrix.platform }}.tar.gz.sig" | head -1)
if [ -f "$SIG_FILE" ]; then
SIGNATURE=$(cat "$SIG_FILE")
else
SIGNATURE=""
fi
# Find the tar.gz file name for this platform
TAR_FILE=$(find artifacts -name "*-${{ matrix.platform }}.tar.gz" ! -name "*.sig" | head -1)
TAR_NAME=$(basename "$TAR_FILE" 2>/dev/null || echo "OpenChamber-${{ matrix.platform }}.app.tar.gz")
cat > artifacts/latest-${{ matrix.platform }}.json << EOF
{
"version": "${VERSION}",
"notes": "See release notes at https://github.com/${{ github.repository }}/releases/tag/v${VERSION}",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"${{ matrix.platform }}": {
"signature": "${SIGNATURE}",
"url": "https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${TAR_NAME}"
}
}
}
EOF
echo "Generated latest-${{ matrix.platform }}.json:"
cat artifacts/latest-${{ matrix.platform }}.json
- name: Upload release assets
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.create-release.outputs.version }}
files: |
artifacts/*.dmg
artifacts/*.tar.gz
artifacts/*.tar.gz.sig
artifacts/latest-${{ matrix.platform }}.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload manifest as artifact
uses: actions/upload-artifact@v4
with:
name: manifest-${{ matrix.platform }}
path: artifacts/latest-${{ matrix.platform }}.json
retention-days: 1
publish-npm:
needs: create-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build packages
run: bun run build
- name: Create npm tarball
working-directory: packages/web
run: npm pack
- name: Upload npm tarball to release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.create-release.outputs.version }}
files: packages/web/*.tgz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish to npm
if: ${{ github.event.inputs.dry_run != 'true' }}
working-directory: packages/web
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
combine-manifests:
needs: [create-release, build-desktop-macos]
runs-on: ubuntu-latest
steps:
- name: Download aarch64 manifest
uses: actions/download-artifact@v4
with:
name: manifest-darwin-aarch64
path: artifacts
- name: Download x86_64 manifest
uses: actions/download-artifact@v4
with:
name: manifest-darwin-x86_64
path: artifacts
- name: Combine manifests
run: |
VERSION="${{ needs.create-release.outputs.version }}"
PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
REPO="${{ github.repository }}"
# Validate that both manifest files exist and are valid JSON
if [ ! -f artifacts/latest-darwin-aarch64.json ]; then
echo "Error: aarch64 manifest not found"
exit 1
fi
if [ ! -f artifacts/latest-darwin-x86_64.json ]; then
echo "Error: x86_64 manifest not found"
exit 1
fi
# Validate JSON structure
if ! jq empty artifacts/latest-darwin-aarch64.json 2>/dev/null; then
echo "Error: aarch64 manifest is not valid JSON"
exit 1
fi
if ! jq empty artifacts/latest-darwin-x86_64.json 2>/dev/null; then
echo "Error: x86_64 manifest is not valid JSON"
exit 1
fi
# Validate platform data exists in manifests
if ! jq -e '.platforms["darwin-aarch64"]' artifacts/latest-darwin-aarch64.json > /dev/null; then
echo "Error: darwin-aarch64 platform data not found in manifest"
exit 1
fi
if ! jq -e '.platforms["darwin-x86_64"]' artifacts/latest-darwin-x86_64.json > /dev/null; then
echo "Error: darwin-x86_64 platform data not found in manifest"
exit 1
fi
# Use jq to properly combine the manifests
jq -n \
--arg version "$VERSION" \
--arg notes "See release notes at https://github.com/${REPO}/releases/tag/v${VERSION}" \
--arg pub_date "$PUB_DATE" \
--slurpfile aarch64 artifacts/latest-darwin-aarch64.json \
--slurpfile x86_64 artifacts/latest-darwin-x86_64.json \
'{
version: $version,
notes: $notes,
pub_date: $pub_date,
platforms: {
"darwin-aarch64": $aarch64[0].platforms["darwin-aarch64"],
"darwin-x86_64": $x86_64[0].platforms["darwin-x86_64"]
}
}' > artifacts/latest.json
echo "Generated combined latest.json:"
cat artifacts/latest.json
- name: Upload combined manifest
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.create-release.outputs.version }}
files: artifacts/latest.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-desktop-electron-macos:
needs: create-release
runs-on: macos-26
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
arch: arm64
platform: darwin-aarch64
- target: x86_64-apple-darwin
arch: x64
platform: darwin-x86_64
steps:
- uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Install Apple Certificate
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
KEYCHAIN_PATH=$RUNNER_TEMP/electron-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 \
-P "$APPLE_CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 \
-k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
- name: Build Electron app
working-directory: packages/electron
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# rebuild-native.mjs reads this to target the right arch when
# cross-building (runner is arm64; x64 matrix needs the hint).
ELECTRON_BUILDER_ARCH: ${{ matrix.arch }}
run: |
bun run build:web-assets
bun run bundle:main
# npmRebuild=false in package.json, so electron-builder won't
# recompile native deps on its own — we must rebuild against the
# target Electron ABI before packaging, otherwise better-sqlite3/
# node-pty/bun-pty crash on require inside the packaged app.
bun run rebuild:native
./node_modules/.bin/electron-builder --mac --${{ matrix.arch }} --publish=never
- name: Verify signature + entitlements + notarization
run: |
set -euo pipefail
APP_DIR="packages/electron/dist/mac"
[ -d "packages/electron/dist/mac-arm64" ] && APP_DIR="packages/electron/dist/mac-arm64"
APP_PATH=$(find "$APP_DIR" -maxdepth 2 -name "*.app" -print -quit)
if [ -z "$APP_PATH" ]; then
echo "Error: .app not found under packages/electron/dist/mac*"
ls -la packages/electron/dist/
exit 1
fi
echo "Verifying $APP_PATH"
codesign -vv --deep --strict "$APP_PATH"
# Require hardened runtime
CS_INFO=$(codesign -dv --verbose=4 "$APP_PATH" 2>&1)
echo "$CS_INFO"
if ! echo "$CS_INFO" | grep -q "flags=.*runtime"; then
echo "Error: hardened runtime flag missing"
exit 1
fi
# Require notary ticket stapled
xcrun stapler validate "$APP_PATH"
ENTITLEMENTS=$(codesign -d --entitlements :- "$APP_PATH" 2>&1 || true)
if echo "$ENTITLEMENTS" | grep -q "com.apple.security.app-sandbox"; then
echo "Error: app sandbox entitlement is present"
exit 1
fi
for key in \
com.apple.security.cs.allow-jit \
com.apple.security.cs.allow-unsigned-executable-memory \
com.apple.security.cs.disable-library-validation
do
if ! echo "$ENTITLEMENTS" | grep -q "<key>$key</key>"; then
echo "Error: required entitlement missing: $key"
exit 1
fi
done
- name: Upload DMG / ZIP / blockmaps to release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.create-release.outputs.version }}
files: |
packages/electron/dist/*.dmg
packages/electron/dist/*.zip
packages/electron/dist/*.blockmap
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload per-arch latest-mac.yml for merge
uses: actions/upload-artifact@v4
with:
name: latest-yml-${{ matrix.target }}
path: packages/electron/dist/latest-mac.yml
retention-days: 1
combine-electron-manifests:
needs: [create-release, build-desktop-electron-macos]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Download per-arch latest-mac.yml
uses: actions/download-artifact@v4
with:
pattern: latest-yml-*-apple-darwin
path: artifacts
- name: Finalize combined latest-mac.yml
env:
LATEST_YML_DIR: ${{ github.workspace }}/artifacts
GH_REPO: ${{ github.repository }}
OPENCHAMBER_VERSION: ${{ needs.create-release.outputs.version }}
run: node packages/electron/scripts/finalize-latest-yml.mjs
- name: Upload combined latest-mac.yml to release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.create-release.outputs.version }}
files: ${{ runner.temp }}/latest-mac.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
finalize-release:
needs: [create-release, build-desktop-macos, build-desktop-electron-macos, publish-npm, combine-manifests, combine-electron-manifests]
runs-on: ubuntu-latest
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
DISCORD_UPDATE_ROLE_ID: ${{ secrets.DISCORD_UPDATE_ROLE_ID }}
steps:
- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.create-release.outputs.version }}
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Send release to Discord
if: ${{ env.DISCORD_WEBHOOK_URL != '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.create-release.outputs.version }}
REPOSITORY: ${{ github.repository }}
UPDATE_ROLE_ID: ${{ env.DISCORD_UPDATE_ROLE_ID }}
run: |
node - <<'NODE'
(async () => {
const tag = `v${process.env.VERSION}`;
const repo = process.env.REPOSITORY;
const rawRoleId = (process.env.UPDATE_ROLE_ID || '').trim();
const updateRoleId = /^\d+$/.test(rawRoleId) ? rawRoleId : '';
const releaseRes = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${tag}`, {
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
Accept: 'application/vnd.github+json',
},
});
if (!releaseRes.ok) {
const body = await releaseRes.text();
throw new Error(`Failed to fetch release ${tag}: ${releaseRes.status} ${body}`);
}
const release = await releaseRes.json();
const description = (release.body || `OpenChamber ${tag} released.`).slice(0, 4096);
const mention = updateRoleId ? `<@&${updateRoleId}>` : '';
const payload = {
username: 'OpenChamber Releases',
...(mention ? { content: mention } : {}),
...(updateRoleId
? {
allowed_mentions: {
roles: [updateRoleId],
},
}
: {}),
embeds: [
{
title: release.name || `OpenChamber ${tag}`,
url: release.html_url,
description,
color: 2105893,
footer: { text: 'OpenChamber Changelog' },
},
],
};
const discordRes = await fetch(process.env.DISCORD_WEBHOOK_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
});
if (!discordRes.ok) {
const body = await discordRes.text();
throw new Error(`Failed to send Discord release: ${discordRes.status} ${body}`);
}
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE
- name: Trigger openchamber-website site refresh (optional)
env:
WEBSITE_REPO: openchamber/openchamber-website
WEBSITE_TOKEN: ${{ secrets.OPENCHAMBER_WEBSITE_REPO_TOKEN }}
VERSION: ${{ needs.create-release.outputs.version }}
run: |
if [ -z "$WEBSITE_TOKEN" ]; then
echo "OPENCHAMBER_WEBSITE_REPO_TOKEN not set; skip site refresh dispatch."
exit 0
fi
curl --fail-with-body -sS -X POST \
-H "Authorization: Bearer $WEBSITE_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/$WEBSITE_REPO/dispatches \
-d @- <<JSON
{
"event_type": "site_refresh_requested",
"client_payload": {
"source_repo": "${{ github.repository }}",
"release_tag": "v$VERSION"
}
}
JSON
-60
View File
@@ -1,60 +0,0 @@
name: Publish VS Code Extension
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: write
jobs:
publish:
runs-on: ubuntu-latest
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build VS Code extension
run: bun run --cwd packages/vscode build
- name: Package extension
run: cd packages/vscode && bunx vsce package --no-dependencies
- name: Publish to VS Code Marketplace
if: ${{ env.VSCE_PAT != '' }}
run: cd packages/vscode && bunx vsce publish -p "$VSCE_PAT" --no-dependencies
- name: Publish to Open VSX
if: ${{ env.OVSX_PAT != '' }}
run: bunx ovsx publish packages/vscode/*.vsix -p "$OVSX_PAT"
- name: Upload VSIX artifact
uses: actions/upload-artifact@v4
with:
name: openchamber-vscode-vsix
path: packages/vscode/*.vsix
- name: Attach VSIX to GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
files: packages/vscode/*.vsix
generate_release_notes: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-59
View File
@@ -1,59 +0,0 @@
# Logs
logs
*.log
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
node_modules
dist
dist-ssr
release
*.local
*.tgz
*.vsix
/npm
/tsc
/openchamber@*
local-dev*
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.opencode/plans/*
.hive
docs/personal/*
# Build outputs
build/
.gradle/
.*.bun-build
# Built webview assets (generated during build)
src/main/resources/webview/
# IDE
*.iml
*.ipr
*.iws
# Local env
.env
.env.local
# IntelliJ plugin (separate project)
packages/intellij/
# OS
Thumbs.db
# Local runtime state (docker/dev)
data/
workspaces/
*.pid
-1
View File
@@ -1 +0,0 @@
lts
-37
View File
@@ -1,37 +0,0 @@
---
description: Draft user-facing CHANGELOG.md entries for [Unreleased]
agent: build
---
You are updating @CHANGELOG.md and @packages/vscode/CHANGELOG.md.
Goal: write user-facing bullet points for the `## [Unreleased]` section that summarize the changes since the latest git tag up to `HEAD`.
Style rules:
- Match the writing style of the existing changelog (tone + level of detail).
- User-facing and benefit-oriented; avoid internal component names unless users see them (ex: "VS Code extension", "Desktop app", "Web app").
- For @packages/vscode/CHANGELOG.md: Craft entries specifically for the VS Code extension. Exclude features or fixes specific to the Desktop app, Web app, or Mobile/PWA. Focus on core UI improvements and VS Code integration. Do NOT use "VSCode:" or "VS Code:" prefixes in this file.
- Prefer 5-9 bullets; group by platform only if it reads better.
- No new release header; only update the `[Unreleased]` bullets.
- Don't include implementation notes, commit hashes, or file paths in the changelog text.
- Use area prefixes when helpful for grouping in the main @CHANGELOG.md (e.g., "Chat:", "VSCode:", "Settings:", "Git:", "Terminal:", "Mobile:", "UI:").
- Credit contributors inline using "(thanks to @username)" at the end of the bullet. Find contributor usernames from commit authors or PR metadata when available. Skip if contributor is btriapitsyn, since this is a repo owner.
Determine the base version:
- Use the latest tag (ex: `v1.3.2`) as the base.
- Inspect all commits after the base up to `HEAD`.
Repo context for style:
!`head -140 CHANGELOG.md`
Git context (base tag, commits, changed files):
!`BASE=$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD); echo "Base: $BASE"; echo "Commits since base: $(git rev-list --count "$BASE"..HEAD)"; echo "Diff stats: $(git diff --shortstat "$BASE"..HEAD)"; echo; echo "=== Top 30 commits ==="; git log --oneline -30 "$BASE"..HEAD; echo; echo "=== Changed files ==="; git diff --stat "$BASE"..HEAD`
Additional hints (optional, use only if needed):
- If there are breaking changes or user-visible behavior changes, call them out first.
- If changes are mostly internal refactors, summarize them as reliability/performance improvements.
Now:
1) Propose the new `[Unreleased]` bullet list for the main @CHANGELOG.md.
2) Propose the VS Code-specific `[Unreleased]` list for @packages/vscode/CHANGELOG.md.
3) Edit both files to update their respective `[Unreleased]` sections.
-376
View File
@@ -1,376 +0,0 @@
{
"name": ".opencode",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@opencode-ai/plugin": "1.4.10"
}
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@opencode-ai/plugin": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.4.10.tgz",
"integrity": "sha512-35Za2LT2oNWnBoonmPjN1Z9PB4+ir2a6GbZ3nIZQQL/96mqzTRkT1FqUkQc3bdMmfT1R1rqOd5aMzkIXMqC7dA==",
"license": "MIT",
"dependencies": {
"@opencode-ai/sdk": "1.4.10",
"effect": "4.0.0-beta.48",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.1.100",
"@opentui/solid": ">=0.1.100"
},
"peerDependenciesMeta": {
"@opentui/core": {
"optional": true
},
"@opentui/solid": {
"optional": true
}
}
},
"node_modules/@opencode-ai/sdk": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.4.10.tgz",
"integrity": "sha512-Yaddcs/COp0hwiCxgobSZyDUN0nHgkEFL4bG0BQxwd52SGAysOr6A6L0ihfkuhVx0kbi9eXWgZk4ydNOrnur5w==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/effect": {
"version": "4.0.0-beta.48",
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz",
"integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"fast-check": "^4.6.0",
"find-my-way-ts": "^0.1.6",
"ini": "^6.0.0",
"kubernetes-types": "^1.30.0",
"msgpackr": "^1.11.9",
"multipasta": "^0.2.7",
"toml": "^4.1.1",
"uuid": "^13.0.0",
"yaml": "^2.8.3"
}
},
"node_modules/fast-check": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.6.0.tgz",
"integrity": "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^8.0.0"
},
"engines": {
"node": ">=12.17.0"
}
},
"node_modules/find-my-way-ts": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz",
"integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==",
"license": "MIT"
},
"node_modules/ini": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
"integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/kubernetes-types": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz",
"integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==",
"license": "Apache-2.0"
},
"node_modules/msgpackr": {
"version": "1.11.9",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.9.tgz",
"integrity": "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==",
"license": "MIT",
"optionalDependencies": {
"msgpackr-extract": "^3.0.2"
}
},
"node_modules/msgpackr-extract": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build-optional-packages": "5.2.2"
},
"bin": {
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
},
"optionalDependencies": {
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
}
},
"node_modules/multipasta": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz",
"integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==",
"license": "MIT"
},
"node_modules/node-gyp-build-optional-packages": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.1"
},
"bin": {
"node-gyp-build-optional-packages": "bin.js",
"node-gyp-build-optional-packages-optional": "optional.js",
"node-gyp-build-optional-packages-test": "build-test.js"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pure-rand": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz",
"integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/toml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz",
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/yaml": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
@@ -1,216 +0,0 @@
---
name: clack-cli-patterns
description: Use when creating or modifying terminal CLI commands, prompts, or output formatting in OpenChamber. Enforces Clack UX standards with strict parity and safety across TTY/non-TTY, --quiet, and --json modes.
license: MIT
compatibility: opencode
---
## Overview
OpenChamber terminal CLI uses `@clack/prompts` for interactive UX, but command policy and validation must be mode-agnostic.
**Core principle:** policy-first, UX-second. Clack is presentation, not enforcement.
## Scope
Use this skill for terminal CLI work only (for example `packages/web/bin/*`).
Do not use this skill for web UI or VS Code webview styling work.
## Mandatory Rules
1. **Validation first**
- Safety and correctness checks must run in all modes.
- Prompts may help collect input, but cannot be the only guard.
2. **Mode parity is required**
- Behavior must be equivalent in:
- Interactive TTY
- Non-interactive shells
- `--quiet`
- `--json`
- Fully pre-specified flags
- Invalid operations must fail deterministically with non-zero exit code.
3. **Prompt guard contract**
- Only prompt when all are true:
- stdout is TTY
- not `--quiet`
- not `--json`
- not automated/non-interactive context
4. **Output contract**
- `--json`: machine-readable output only.
- `--quiet`: suppress non-essential output only.
- Neither mode weakens policy enforcement.
5. **Cancellation contract**
- Handle prompt cancellation with `isCancel` + `cancel(...)`.
- Handle SIGINT cleanly and use consistent exit semantics.
## Clack Primitive Standard
- **Flow framing:** `intro`, `outro`, `cancel`
- **Status lines:** `log.info`, `log.success`, `log.warn`, `log.error`, `log.step`
- **Guidance blocks:**
- default: `note`
- high-severity warnings only: `box`
- **Prompts:** `select`, `confirm`, `text`, `password`
- **Long-running feedback:**
- unknown duration: `spinner`
- known duration: `progress`
- multi-stage: `tasks`
## Preferred Pattern
Centralize Clack imports and formatting helpers in one adapter module (for example `cli-output.js`) so command logic stays focused on behavior and policy.
### Thin framework (recommended)
Use a small shared helper surface rather than command-specific formatting logic.
- `isJsonMode(options)`
- `isQuietMode(options)`
- `shouldRenderHumanOutput(options)`
- `canPrompt(options)`
- `createSpinner(options)`
- `createProgress(options, config)`
- `printJson(payload)`
Keep this layer minimal. Do not hide core validation or command semantics inside output helpers.
## Output Contracts by Mode
### `--quiet` contract
`--quiet` should still return essential result data.
- Read/list commands: emit concise machine-friendly lines (not framed Clack blocks).
- Action commands: emit one minimal success line and concise errors.
- Do not suppress required outcomes entirely.
Quiet output should still be complete enough for scripts and quick human scanning.
- Status-like commands should list all active items, not only `running`/`ok`.
- Prefer compact stable key tokens in quiet lines (for example `port 3000 pass:yes`).
### `--json` contract (strict)
- Output must be JSON only (no extra text before/after payload).
- Warnings/info should be represented in JSON fields (for example `status`, `messages`).
- Preserve non-zero exit codes for failures.
## Human UX Consistency
### Framing completeness
- If human flow uses `intro`, close with `outro` (or `outro('')` when you want structure without text).
- Avoid orphan frame/spinner artifacts (prefer `spinner.clear()` when a trailing spinner line is not wanted).
- If a structured summary section immediately follows a spinner, prefer `spinner.clear()` to avoid duplicate success lines.
### Progress feedback for visible operations
- For operations users wait on (start/stop/restart/tunnel lifecycle), show in-progress spinner in interactive mode.
- Resolve each spinner explicitly to done/error so users can see completion state at the same visual location.
- Keep quiet/json modes non-animated.
### Prompt flow design
- Ask required inputs in dependency order (for example hostname before token when token depends on chosen host/mode context).
- When offering save-vs-run flows, ask intent before collecting optional metadata (for example profile name only if user chooses save).
- Prefill editable values with `initialValue` (not only `placeholder`) so users can accept or edit quickly.
- Reuse latest relevant values when safe (for example last managed-local config path, last managed-remote hostname).
### Readability on narrow terminals
- Prefer short lines.
- Split long guidance into multiple detail lines.
- Use warning/info codes (`[CODE]`) when the message has follow-up docs or repeat use.
### Guidance tone
- Use `Optional Tips` for non-required next actions.
- Avoid wording that implies mandatory follow-up unless it is truly required.
### Guidance rendering style (preferred)
- Prefer structured status lines for reusable hints:
- `logStatus('info', '[CODE]', '<actionable command or short guidance>')`
- Use short, stable codes (for example `[START_PROFILE]`, `[PORT_MISMATCH]`) so users can quickly scan and recognize repeated guidance.
- Prefer this style over boxed notes for routine follow-up actions.
- Reserve `note`/boxed callouts for rare, high-context guidance where a long paragraph is truly necessary.
## Parity Verification Matrix
For each command/subcommand, manually verify:
1. default interactive TTY output
2. `--quiet` output (minimal but informative)
3. `--json` output (JSON-only)
4. non-TTY behavior (e.g. piped)
5. error path in both human and json modes
## Copy/Paste Snippets
### Prompt Guard
```js
if (canPrompt(options)) {
const value = await select({
message: 'Choose an option',
options: [{ value: 'a', label: 'Option A' }],
});
if (isCancel(value)) {
cancel('Operation cancelled.');
return;
}
}
```
### Non-Interactive Fallback
```js
if (!resolvedValue) {
if (canPrompt(options)) {
// prompt path
} else {
throw new Error('Missing required value. Provide --flag <value>.');
}
}
```
### Spinner Guard
```js
const spin = createSpinner(options);
spin?.start('Running operation...');
// ...work...
spin?.stop('Done');
```
### JSON vs Human Output
```js
if (options.json) {
printJson({ ok: true, data });
return;
}
intro('Operation');
log.success('Completed');
outro('done');
```
## Implementation Checklist
1. Add or update core validators first.
2. Ensure validators execute in all modes.
3. Add interactive Clack UX only as enhancement.
4. Verify parity between interactive and non-interactive flows.
5. Ensure script-safe deterministic failure behavior.
## References
- Policy source: `AGENTS.md` (CLI Parity and Safety Policy)
- Terminal CLI precedent: `packages/web/bin/cli.js`
- Output adapter precedent: `packages/web/bin/cli-output.js`
@@ -1,128 +0,0 @@
---
name: locale-ui-patterns
description: Use when creating or modifying OpenChamber UI text, labels, buttons, placeholders, aria labels, empty states, toasts, dialogs, settings copy, navigation labels, or any user-facing strings.
---
# Locale UI Patterns
## Core Rule
User-facing UI text must go through `@/lib/i18n`; do not hardcode English strings in components.
Use this skill for any React UI change that adds or edits visible text, accessible labels, placeholders, tooltips, toasts, dialogs, settings labels, navigation labels, or empty/error states.
## Required Flow
1. Add or reuse a key in `packages/ui/src/lib/i18n/messages/en.ts`.
2. Add the same key to every non-English dictionary in `packages/ui/src/lib/i18n/messages/`.
3. In components, call `const { t } = useI18n()` from `@/lib/i18n` and render `t('key')`.
4. For locale names or language picker labels, use `label(locale)` from `useI18n()`.
5. Keep locale state in `packages/ui/src/lib/i18n/*`; do not add locale fields to broad stores like `useUIStore`.
6. Do not remount the app to update language. Components must re-render through `useI18n()`.
## Component Usage Rules
- Import from `@/lib/i18n`, not deep files.
- Keep `t(...)` calls inside React render/hook scope so locale changes re-render text.
- Do not resolve translated text at module scope.
- For static option arrays, store `labelKey` / `descriptionKey`; resolve with `t(...)` inside the component.
- For non-React helpers, pass translated strings in from the component or pass `t` explicitly.
## Key Style
Use stable semantic keys, not English text as keys.
Keys should describe location + UI role + meaning. They should not encode current copy wording.
Use existing nearby naming when extending a surface. If no nearby pattern exists, choose a short path that mirrors the UI ownership.
Namespaces like `layout.*`, `settings.*`, `chat.*`, `git.*`, `session.*`, `toast.*`, and `dialog.*` are examples, not a fixed exhaustive list.
Good:
```ts
'settings.appearance.language.label': 'Language'
'layout.mainTab.chat': 'Chat'
'chat.input.placeholder': 'Ask OpenChamber...'
```
Bad:
```ts
'Language': 'Language'
'chatLabel': 'Chat'
'askOpenChamberDotDotDot': 'Ask OpenChamber...'
```
Avoid overly generic keys unless the text is truly global and context-independent. Prefer specific keys when button meaning can vary by surface.
## Parameters
Use `{name}` placeholders for dynamic values.
```ts
'toast.language.changed': 'Language changed to {language}'
```
```tsx
t('toast.language.changed', { language: label(locale) })
```
Do not pass grammar fragments as params. Never use params like `{suffix}`, `{plural}`, `{article}`, `{prefix}`, `{dateSuffix}`, or pieces of words/sentences.
Bad:
```tsx
t('dialog.delete.description', { count, suffix: count === 1 ? '' : 's' })
```
Good:
```tsx
count === 1
? t('dialog.delete.descriptionSingle', { count })
: t('dialog.delete.descriptionPlural', { count })
```
Plural/count-dependent text must use separate complete-message keys unless all supported locales can use one identical complete sentence. Placeholders are only for real values (`{count}`, `{name}`, `{path}`), not grammar.
Optional clauses must also be complete-message keys. Do not build a sentence by injecting a translated phrase into another translated sentence.
Bad:
```tsx
t('dialog.delete.description', {
dateLabel: date ? t('dialog.delete.dateSuffix', { date }) : '',
})
```
Good:
```tsx
date
? t('dialog.delete.descriptionWithDate', { count, date })
: t('dialog.delete.description', { count })
```
## What Counts As UI Text
- Button and menu labels
- Settings labels and descriptions
- Placeholder text
- Tooltip content
- Dialog titles/descriptions/actions
- Toast title/description/action labels
- Empty/error/loading states
- `aria-label`, `title`, image `alt` text when user-facing
## Exceptions
Do not translate:
- Product names: `OpenChamber`, `OpenCode`, `GitHub`
- Protocol/tool acronyms: `MCP`, `SSE`, `WebSocket`, `API`
- Model/provider names
- File paths, command names, environment variables
- User/generated content
## Review Checklist
- No new hardcoded user-facing English in changed UI files.
- Every new key exists in all dictionaries.
- No locale state added to broad/shared stores.
- No full app remount for locale changes.
- Locale switch preserves current UI state.
@@ -1,238 +0,0 @@
---
name: settings-ui-patterns
description: Use when creating or modifying UI components, styling, or visual elements related to Settings in OpenChamber.
license: MIT
compatibility: opencode
---
# Settings UI Patterns Skill
## Purpose
This skill provides instructions for creating or redesigning Settings pages, informational panels, and configuration interfaces within the OpenChamber application.
## Current Canonical Look (2026)
Use this as source of truth for new settings UI work.
- **Flat hierarchy first**: Prefer spacing + typography hierarchy over boxed backgrounds.
- **No unnecessary wrappers**: Avoid extra section wrappers that mix unrelated controls.
- **No redundant section titles**: Do not add headers like `Theme Preferences` or `Scaling & Layout` when controls are already self-explanatory.
- **Compact controls**: Option chips and radio rows should be dense, not tall.
- **Left-leading state icon**: Radio/checkbox state icon appears before text.
- **Subtle state contrast**: Inactive radio labels should be visibly dimmer than active labels.
- **Minimal row chrome**: Avoid row hover/background highlighting by default; keep only where explicitly needed.
## Typography Guidelines
Always utilize the standard OpenChamber typography classes defined in `packages/ui/src/lib/typography.ts`.
- **Page Title**: Use `typography-ui-header font-semibold text-foreground` for the top-most title of a settings page/dialog.
- **Section Header**: Use `typography-ui-header font-medium text-foreground` for settings sections (e.g. `Notification Events`, `Session Defaults`).
- **Control Group Header**: Use `typography-ui-header font-medium text-foreground` (or `font-normal` if it reads too loud) for grouped controls inside a section (e.g. `Default Tool Output`, `Diff Layout`).
- **Values / Primary Text**: Use `typography-ui-label text-foreground`. Add `tabular-nums` if displaying numbers or stats to ensure vertical alignment.
- **Option Labels**: Use non-bold label text in compact option controls (`font-normal` when needed to override).
- **Meta / Helper Text**: Use `typography-meta text-muted-foreground` or `typography-small text-muted-foreground` for supplemental text.
## Layout and Spacing Patterns
### 1. Main Backgrounds
Main wrappers should generally use `bg-background` or `bg-[var(--surface-background)]`. Ensure adequate padding (e.g., `px-5 py-6` or `p-6`).
### 2. Subsection Grouping
Group related controls with vertical spacing, not mandatory cards.
- Use `space-y-3` between logical subsections.
- Use `p-2` for subsection internal padding.
- Avoid adding `bg-[var(--surface-elevated)]` unless there is a clear reason.
- Avoid extra row decorations (`rounded-md`, hover fills) unless there is explicit UX value.
### 3. Header-to-Content Hierarchy (critical)
When removing cards/background wrappers, spacing must be rebalanced so header ownership stays clear.
- Keep **section-to-section spacing larger** than **header-to-own-content spacing**.
- Typical pattern:
- header wrapper `mb-1 px-1`
- content wrapper `pt-0 pb-2 px-2`
- outer section spacing `mb-8`
- Do not leave legacy `mb-3` style gaps after flattening a section; it makes headers look detached.
### 4. Headerless Blocks (when context is obvious)
If the page title already provides enough context, remove redundant local headers and place controls directly below the title.
- Example: project page identity controls can sit directly under project name/path.
- Tighten top gap for this pattern (e.g. top header `mb-4` instead of larger section spacing).
```tsx
<div className="space-y-3">
<section className="p-2">...</section>
<section className="p-2">...</section>
</div>
```
## Structural Patterns
### 1. Segmented Option Buttons (compact)
Use for short option sets where button-style segmented choice reads best (e.g. Default Tool Output).
```tsx
<div className="mt-1 flex flex-wrap items-center gap-1">
<ButtonSmall
variant="outline"
size="xs"
className={cn('!font-normal', isSelected ? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10' : 'text-foreground')}
>
Collapsed
</ButtonSmall>
</div>
```
### 2. Radio Option Lists (compact rows)
Use for mutually exclusive mode/layout settings (e.g. Diff Layout, Diff View Mode).
- Use shared `Radio` component from `@/components/ui/radio`.
- Icon first, label second.
- Row container compact: `py-0.5`.
- Inactive label can use `text-foreground/50`.
```tsx
<div role="radiogroup" aria-label="Diff layout" className="mt-1 space-y-0">
<div className="flex w-full items-center gap-2 py-0.5">
<Radio checked={selected} onChange={onSelect} ariaLabel="Diff layout: Dynamic" />
<span className={cn('typography-ui-label font-normal', selected ? 'text-foreground' : 'text-foreground/50')}>Dynamic</span>
</div>
</div>
```
### 3. Checkbox Setting Rows
Use shared `Checkbox` component from `@/components/ui/checkbox` for boolean toggles.
- Icon first, text immediately after (`gap-2`).
- Typical row spacing for checkbox rows: `py-1.5`.
- Keep row click and keyboard toggle support.
- Prefer checkbox over binary show/hide button pairs for pure boolean state.
```tsx
<div
className="group flex cursor-pointer items-center gap-2 py-1.5"
role="button"
tabIndex={0}
>
<Checkbox checked={value} onChange={setValue} ariaLabel="Show Dotfiles" />
<span className="typography-ui-label text-foreground">Show Dotfiles</span>
</div>
```
### 4. Invisible Two-Column Alignment
Use consistent label/control columns across settings rows so controls align on a shared vertical line.
- Desktop row pattern: `flex items-center gap-8`
- Label column width: `w-56 shrink-0`
- Control cluster: `w-fit`
```tsx
<div className="flex items-center gap-8 py-1.5">
<span className="typography-ui-label text-foreground w-56 shrink-0">Interface Font Size</span>
<div className="flex items-center gap-2 w-fit">...</div>
</div>
```
#### Disabled control rule
If a control is unavailable, disable the control only. Do not dim the label row by default.
#### Width-matching rule
When matching visual widths across different rows, compare full row footprint (control + adjacent action buttons), not just input width.
### 5. Theme Row Composition
For theme controls in Appearance:
- `Color Mode` header on first line; option chips below it.
- `Light Theme` and `Dark Theme` on one row where possible, wrapping on small widths.
- Keep selectors near labels and aligned to existing column rhythm.
- Replace persistent helper text with an info tooltip icon near the related action.
```tsx
<div className="grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
<div className="flex min-w-0 items-center gap-2">Light Theme ...</div>
<div className="flex min-w-0 items-center gap-2">Dark Theme ...</div>
</div>
```
### 6. Numeric Controls in Settings
Use compact stepper input (`- value +`) plus reset button.
- Prefer shared `NumberInput` stepper style over slider + numeric combo in dense settings pages.
- Keep reset button adjacent to control (`gap-2`).
- Avoid using Tailwind `overflow-hidden` on mobile for controls; `packages/ui/src/styles/mobile.css` forces `.overflow-hidden { overflow-y: auto !important; }`.
Use `overflow-x-hidden overflow-y-hidden` if you truly need clipping.
- Touch devices: `packages/ui/src/styles/mobile.css` enforces `min-height: 36px` on `button`. If you build custom segmented controls with `<button>`, ensure the container height can accommodate that (e.g. `h-9`).
#### Optional numeric overrides
For "override unless empty" fields (e.g. agent Temperature/Top P), keep the value optional and provide a fallback for stepping.
```tsx
<NumberInput
value={temperature}
fallbackValue={0.7}
onValueChange={setTemperature}
onClear={() => setTemperature(undefined)}
min={0}
max={2}
step={0.1}
inputMode="decimal"
emptyLabel="—"
/>
```
```tsx
<div className="flex items-center gap-2 w-fit">
<NumberInput value={fontSize} onValueChange={setFontSize} min={50} max={200} step={5} />
<ButtonSmall variant="ghost" className="h-7 w-7 px-0">...</ButtonSmall>
</div>
```
### 7. Inputs and Select Triggers (settings density)
Keep form controls in settings compact and aligned.
- Prefer `Input` with `className="h-7"` in dense settings rows.
- Prefer default `SelectTrigger` sizing (avoid `size="lg"` in settings).
- For icon-only actions next to inputs, use `ButtonSmall` with `h-7 w-7 p-0`.
```tsx
<div className="flex items-center gap-2">
<Input className="h-7" />
<ButtonSmall variant="outline" size="xs" className="h-7 w-7 p-0" aria-label="Browse">
<RiFolderLine className="h-4 w-4" />
</ButtonSmall>
</div>
```
### 8. Template Grids (text fields)
For template-like settings (title/message pairs), use a simple grid and flat cells.
- Grid: `grid grid-cols-1 gap-2 md:grid-cols-2 md:gap-3`
- Cell: `section p-2`
- Field: `Input className="h-7"`
### 9. Icon/Color Picker Rows
For dense icon/color pickers in settings:
- Place options under the field label when they are a palette/grid choice.
- Use stable selected-state styling (`border`/`ring`/subtle background), avoid transform jumps (`scale-*`).
- Keep chip size compact (`h-7 w-7`) and spacing consistent (`gap-2`).
## Control Selection Rules
- **Use compact option buttons** for short, chip-like selection groups.
- **Use radios** for explicit mode/layout choices where list scanning is better.
- **Use checkboxes** for true/false settings.
- **Avoid show/hide button pairs** when a checkbox maps directly to the boolean.
- **Do not couple unrelated toggles** under one synthetic section header; keep hierarchy clear.
## Best Practices
- **Density**: Keep options compact; avoid oversized rows/chips in dense settings pages.
- **Consistency**: Reuse shared controls (`Checkbox`, `Radio`, `ButtonSmall size="xs"`) instead of inline icon logic.
- **Reuse via composition**: Prefer a single settings component with a `visibleSettings` subset (like `OpenChamberVisualSettings`) for multiple tabs (Appearance/Chat) instead of duplicating markup.
- **Hierarchy**: Page title = `font-semibold`; section header = `font-medium`; control group header = `font-medium` (or `font-normal` if needed); option labels = non-bold.
- **Subsection depth**: Nested subgroup headings under a section should usually be one step lighter than parent heading weight.
- **Hierarchy sanity check**: after flattening UI, verify visual grouping by spacing first (not color).
- **Helper blocks**: For small notes/errors under a section, use `mt-1 px-2` with `typography-meta text-muted-foreground/70` (and status token for errors).
- **Truncation**: Always consider long text. Use `min-w-0 flex-1 truncate` on text containers that sit next to buttons or icons to prevent layout breakage.
- **Theme Variables**: *Always* use CSS variables for colors (e.g., `var(--status-success)`) rather than hardcoded hex values or generic Tailwind colors when indicating semantic states.
-242
View File
@@ -1,242 +0,0 @@
---
name: theme-system
description: Use when creating or modifying UI components, styling, or visual elements in OpenChamber. All UI colors must use theme tokens - never hardcoded values or Tailwind color classes.
license: MIT
compatibility: opencode
---
## Overview
OpenChamber uses a JSON-based theme system. Themes are defined in `packages/ui/src/lib/theme/themes/`. Users can also add custom themes via `~/.config/openchamber/themes/`.
**Core principle:** UI colors must use theme tokens - never hardcoded hex colors or Tailwind color classes.
## When to Use
- Creating or modifying UI components
- Working with colors, backgrounds, borders, or text
## Quick Decision Tree
1. **Code display?**`syntax.*`
2. **Feedback/status?**`status.*`
3. **Primary CTA?**`primary.*`
4. **Interactive/clickable?**`interactive.*`
5. **Background layer?**`surface.*`
6. **Text?**`surface.foreground` or `surface.mutedForeground`
## Critical Rules
- `surface.elevated` = inputs, cards, panels
- `interactive.hover` = **ONLY on clickable elements**
- `interactive.selection` = active/selected states (not primary!)
- Status colors = **ONLY for actual feedback** (errors, warnings, success)
- Input footers = `bg-transparent` on elevated background
## Button Rules (MANDATORY)
Use only the shared `Button` component from `packages/ui/src/components/ui/button.tsx`.
- Do not create wrapper button components (for example `ButtonLarge`, `ButtonSmall`).
- Do not hardcode button height/padding classes when a `size` variant exists.
- Use semantic button variants consistently; avoid ad-hoc one-off button styling.
### Allowed Button Variants
| Variant | Use for | Token direction |
|-------|-------|-------|
| `default` | Primary action in a local section/dialog | `primary.*` |
| `outline` | Secondary visible action | `surface.elevated` + `interactive.*` |
| `secondary` | Soft secondary action | `interactive.hover` / `interactive.active` |
| `ghost` | Low-emphasis row/toolbar action | transparent + `interactive.hover` |
| `destructive` | Destructive actions (`Delete`, `Revert all`) | `status.error*` |
| `link` | Rare inline text action only | text-link style |
### Allowed Button Sizes
| Size | Use for |
|------|---------|
| `xs` | Dense controls in rows/lists |
| `sm` | Default compact action buttons |
| `default` | Standard form/page actions |
| `lg` | Prominent large actions |
| `icon` | Icon-only square button |
### Button Selection Quick Guide
1. Main CTA in section/dialog -> `default`
2. Side action next to CTA -> `outline`
3. Quiet auxiliary action -> `ghost`
4. Dangerous action -> `destructive`
5. Tiny row action -> keep same variant, set `size="xs"`
### Never Use
- Hardcoded hex colors (`#FF0000`)
- Tailwind colors (`bg-white`, `text-blue-500`, `bg-gray-*`)
- Deprecated: `bg-secondary`, `bg-muted`
## Usage
### Via Hook
```tsx
import { useThemeSystem } from '@/contexts/useThemeSystem';
const { currentTheme } = useThemeSystem();
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
```
### Via CSS Variables
```tsx
<div className="bg-[var(--surface-elevated)] hover:bg-[var(--interactive-hover)]">
```
## Color Tokens
### Surface Colors
| Token | Usage |
|-------|-------|
| `surface.background` | Main app background |
| `surface.elevated` | Inputs, cards, panels, popovers |
| `surface.muted` | Secondary backgrounds, sidebars |
| `surface.foreground` | Primary text |
| `surface.mutedForeground` | Secondary text, hints |
| `surface.subtle` | Subtle dividers |
### Interactive Colors
| Token | Usage |
|-------|-------|
| `interactive.border` | Default borders |
| `interactive.hover` | Hover on **clickable elements only** |
| `interactive.selection` | Active/selected items |
| `interactive.selectionForeground` | Text on selection |
| `interactive.focusRing` | Focus indicators |
### Status Colors
| Token | Usage |
|-------|-------|
| `status.error` | Errors, validation failures |
| `status.warning` | Warnings, cautions |
| `status.success` | Success messages |
| `status.info` | Informational messages |
Each has variants: `*`, `*Foreground`, `*Background`, `*Border`.
### Primary Colors
| Token | Usage |
|-------|-------|
| `primary.base` | Primary CTA buttons |
| `primary.hover` | Hover on primary elements |
| `primary.foreground` | Text on primary background |
**Primary vs Selection:** Primary = "click me" (CTA), Selection = "currently active" (state).
### Syntax Colors
For code display only. Never use for UI elements.
| Token | Usage |
|-------|-------|
| `syntax.base.background` | Code block background |
| `syntax.base.foreground` | Default code text |
| `syntax.base.keyword` | Keywords |
| `syntax.base.string` | Strings |
| `syntax.highlights.diffAdded` | Added lines |
| `syntax.highlights.diffRemoved` | Removed lines |
## Examples
### Input Area
```tsx
const { currentTheme } = useThemeSystem();
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
<textarea className="bg-transparent" />
<div className="bg-transparent">{/* Footer - transparent! */}</div>
</div>
```
### Active Tab
```tsx
<button className={isActive
? 'bg-interactive-selection text-interactive-selection-foreground'
: 'hover:bg-interactive-hover/50'
}>
```
### Error Message
```tsx
<div style={{
color: currentTheme.colors.status.error,
backgroundColor: currentTheme.colors.status.errorBackground
}}>
```
### Card
```tsx
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
<h3 style={{ color: currentTheme.colors.surface.foreground }}>Title</h3>
<p style={{ color: currentTheme.colors.surface.mutedForeground }}>Description</p>
</div>
```
## Wrong vs Right
### Wrong
```tsx
// Hardcoded colors
<div style={{ backgroundColor: '#F2F0E5' }}>
<button className="bg-blue-500">
// Primary for active tab
<Tab className="bg-primary">Active</Tab>
// Hover on static element
<div className="hover:bg-interactive-hover">Static card</div>
// Colored footer on input
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
<textarea />
<div style={{ backgroundColor: currentTheme.colors.surface.muted }}>Footer</div>
</div>
```
### Right
```tsx
// Theme tokens
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
<button style={{ backgroundColor: currentTheme.colors.primary.base }}>
// Selection for active tab
<Tab style={{ backgroundColor: currentTheme.colors.interactive.selection }}>Active</Tab>
// Hover only on clickable
<button className="hover:bg-[var(--interactive-hover)]">Click</button>
// Transparent footer
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
<textarea className="bg-transparent" />
<div className="bg-transparent">Footer</div>
</div>
```
## References
- **[Adding Themes](references/adding-themes.md)** - Built-in and custom themes
## Key Files
- Theme types: `packages/ui/src/types/theme.ts`
- Theme hook: `packages/ui/src/contexts/useThemeSystem.ts`
- CSS generator: `packages/ui/src/lib/theme/cssGenerator.ts`
- Built-in themes: `packages/ui/src/lib/theme/themes/`
@@ -1,51 +0,0 @@
---
title: Adding Themes
---
# Adding Themes
## Custom Themes (User)
Drop a JSON file into `~/.config/openchamber/themes/`. No rebuild needed.
1. Create theme file (e.g., `my-theme.json`)
2. In app: **Settings → Theme → Reload themes**
3. Select from dropdown
See `docs/CUSTOM_THEMES.md` for full format reference.
## Built-in Themes (Development)
### 1. Create JSON Files
Add to `packages/ui/src/lib/theme/themes/`:
- `<id>-light.json`
- `<id>-dark.json`
Use existing themes (e.g., `flexoki-dark.json`) as reference for the full structure.
### 2. Register in presets.ts
```typescript
import mytheme_light_Raw from './mytheme-light.json';
import mytheme_dark_Raw from './mytheme-dark.json';
export const presetThemes: Theme[] = [
// ... existing themes
mytheme_light_Raw as Theme,
mytheme_dark_Raw as Theme,
];
```
### 3. Validate
```bash
bun run type-check && bun run lint && bun run build
```
## Key Files
- Theme types: `packages/ui/src/types/theme.ts`
- Presets: `packages/ui/src/lib/theme/themes/presets.ts`
- Example: `packages/ui/src/lib/theme/themes/flexoki-dark.json`
- Custom themes doc: `docs/CUSTOM_THEMES.md`
-386
View File
@@ -1,386 +0,0 @@
# OpenChamber - AI Agent Reference (verified)
## Core purpose
OpenChamber provides UI runtimes (web/desktop/VS Code) for interacting with an OpenCode server (local auto-start or remote URL). UI uses HTTP + SSE via `@opencode-ai/sdk`.
## Runtime architecture (IMPORTANT)
- `Desktop` (Electron) boots the web server **in the same Node process** as the Electron main, then loads the web UI from `http://127.0.0.1:<port>`. No sidecar subprocess.
- `Desktop` (Tauri, legacy) still spawns `openchamber-server` as a bun-compiled sidecar binary. Kept only for auto-update compatibility with existing Tauri installs.
- All backend logic lives in `packages/web/server/*` (and `packages/vscode/*` for the VS Code runtime). The native shell is not a feature backend.
- The shell is used only for stable native integrations: menu, dialog (open folder), notifications, updater, deep-links, quit confirmation.
### Desktop shell: Electron is the target, Tauri is legacy
- **New desktop work goes into `packages/electron/`.** This is the forward path.
- `packages/desktop/` (Tauri) is kept running in parallel only to preserve auto-update for existing installs until the cutover. Do **not** add features to it; do **not** port bug fixes back unless they actually affect currently-released Tauri users.
- Desktop-side changes (IPC handlers, native integrations, window/quit/notification behavior) land in `packages/electron/main.mjs` + `packages/electron/preload.mjs`. The `__TAURI__` shim exposed by the preload keeps the shared UI working against both shells, so renderer-side code should not branch on shell type.
- Electron imports the server via `@openchamber/web/server/index.js` (workspace dep) and calls `startWebUiServer({...})`. The returned handle has `getPort()` / `stop()`. Notifications flow via an `onDesktopNotification` callback injected at startup — no stdout-parsing IPC.
- Build/release: both shells ship in the same GitHub release today (`.github/workflows/release.yml`). The one-shot Tauri → Electron auto-update migration is documented in `docs/TAURI_TO_ELECTRON_CUTOVER.md`; run that when the user decides to flip.
- After the cutover ships and stabilises, `packages/desktop/` is deleted; this note collapses back to "Desktop is Electron".
## Tech stack (source of truth: `package.json`, resolved: `bun.lock`)
- Runtime/tooling: Bun (`package.json` `packageManager`), Node >=20 (`package.json` `engines`)
- UI: React, TypeScript, Vite, Tailwind v4
- State: Zustand (`packages/ui/src/stores/`)
- UI primitives: Base UI (`@base-ui/react`, primary source for dropdown/select/dialog/menu/tooltip/etc. — wrappers live in `packages/ui/src/components/ui/`), Radix UI (`package.json` deps, legacy usages being migrated), HeroUI (`package.json` deps), Remixicon (`package.json` deps)
- Server: Express (`packages/web/server/index.js`)
- Desktop (forward): Electron 41 (`packages/electron/`)
- Desktop (legacy, maintenance-only): Tauri v2 (`packages/desktop/src-tauri/`)
- VS Code: extension + webview (`packages/vscode/`)
## Monorepo layout
Workspaces are `packages/*` (see `package.json`).
- Shared UI: `packages/ui`
- Web app + server + CLI: `packages/web`
- Desktop shell (Electron — forward): `packages/electron`
- Desktop shell (Tauri — legacy, maintenance-only): `packages/desktop`
- VS Code extension: `packages/vscode`
## Documentation map
Before changing any mapped module, read its module documentation first.
### web
Web runtime and server implementation for OpenChamber.
#### lib
Server-side integration modules used by API routes and runtime services.
##### quota
Quota provider registry, dispatch, and provider integrations for usage endpoints.
- Module docs: `packages/web/server/lib/quota/DOCUMENTATION.md`
##### git
Git repository operations for the web server runtime.
- Module docs: `packages/web/server/lib/git/DOCUMENTATION.md`
##### github
GitHub authentication, OAuth device flow, Octokit client factory, and repository URL parsing.
- Module docs: `packages/web/server/lib/github/DOCUMENTATION.md`
##### opencode
OpenCode server integration utilities including config management, provider authentication, and UI authentication.
- Module docs: `packages/web/server/lib/opencode/DOCUMENTATION.md`
##### notifications
Notification message preparation utilities for system notifications, including text truncation and optional summarization.
- Module docs: `packages/web/server/lib/notifications/DOCUMENTATION.md`
##### terminal
WebSocket protocol utilities for terminal input handling including message normalization, control frame parsing, and rate limiting.
- Module docs: `packages/web/server/lib/terminal/DOCUMENTATION.md`
##### tts
Server-side text-to-speech services and summarization helpers for `/api/tts/*` endpoints.
- Module docs: `packages/web/server/lib/tts/DOCUMENTATION.md`
##### skills-catalog
Skills catalog management including discovery, installation, and configuration of agent skill packages.
- Module docs: `packages/web/server/lib/skills-catalog/DOCUMENTATION.md`
## Build / dev commands (verified)
All scripts are in `package.json`.
- Validate: `bun run type-check`, `bun run lint`
- Build all: `bun run build`
- Desktop build (Electron — primary): `bun run electron:build`
- Desktop dev (Electron): `bun run electron:dev`
- Desktop build (Tauri — legacy): `bun run desktop:build`
- VS Code build: `bun run vscode:build`
- Release smoke build: `bun run release:test` (shell script: `scripts/test-release-build.sh`)
## Runtime entry points
- Web bootstrap: `packages/web/src/main.tsx`
- Web server: `packages/web/server/index.js`
- Web CLI: `packages/web/bin/cli.js` (package bin: `packages/web/package.json`)
- Desktop (Electron — primary): `packages/electron/main.mjs` (boots the web server in-process via `startWebUiServer`, loads web UI over loopback; preload at `packages/electron/preload.mjs` exposes the `__TAURI__` IPC shim so shared UI code is shell-agnostic)
- Desktop (Tauri — legacy): `packages/desktop/src-tauri/src/main.rs`
- VS Code extension host: `packages/vscode/src/extension.ts`
- VS Code webview bootstrap: `packages/vscode/webview/main.tsx`
## OpenCode integration
- UI client wrapper: `packages/ui/src/lib/opencode/client.ts` (imports `@opencode-ai/sdk/v2`)
- SSE hookup: `packages/ui/src/hooks/useEventStream.ts`
- Web server embeds/starts OpenCode server: `packages/web/server/index.js` (`createOpencodeServer`)
- Web runtime filesystem endpoints: search `packages/web/server/index.js` for `/api/fs/`
- External server support: Set `OPENCODE_HOST` (full base URL, e.g. `http://hostname:4096`) or `OPENCODE_PORT`, plus `OPENCODE_SKIP_START=true`, to connect to existing OpenCode instance
## Key UI patterns (reference files)
- Settings shell: `packages/ui/src/components/views/SettingsView.tsx`
- Settings shared primitives: `packages/ui/src/components/sections/shared/`
- Settings sections: `packages/ui/src/components/sections/` (incl `skills/`)
- Chat UI: `packages/ui/src/components/chat/` and `packages/ui/src/components/chat/message/`
- Theme + typography: `packages/ui/src/lib/theme/`, `packages/ui/src/lib/typography.ts`
- Terminal UI: `packages/ui/src/components/terminal/` (uses `ghostty-web`)
## External / system integrations (active)
- Git: `packages/ui/src/lib/gitApi.ts`, `packages/web/server/index.js` (`simple-git`)
- Terminal PTY: `packages/web/server/index.js` (`bun-pty`/`node-pty`)
- Skills catalog: `packages/web/server/lib/skills-catalog/`, UI: `packages/ui/src/components/sections/skills/`
## Agent constraints
- Do not modify `../opencode` (separate repo).
- Do not run git/GitHub commands unless explicitly asked.
- Keep baseline green (run `bun run type-check`, `bun run lint` before finalizing changes).
## Agent code of conduct
- Prefer the smallest correct change.
- Preserve working behavior before improving structure.
- Do not add cleverness where a direct implementation is enough.
- Do not infer critical state from weak signals when a stronger source exists.
- Do not encode policy only in UI; enforce it in core logic.
- Do not hide data loss, partial failure, or fallback behavior. Make it explicit in code.
- Finish work end-to-end: implementation, verification, and cleanup.
## Development rules
- Keep diffs tight; avoid drive-by refactors.
- Follow local precedent; inspect nearby code before introducing new patterns.
- Backend changes: keep web, desktop, and VS Code behavior consistent when they share contracts.
- TypeScript: avoid `any`, blind casts, and shape guessing.
- React: prefer function components + hooks; use classes only when required.
- Control flow: prefer early returns and explicit branching over nested ternaries.
- Styling: Tailwind v4, typography via `packages/ui/src/lib/typography.ts`, theme vars via `packages/ui/src/lib/theme/`.
- Shared UI patterns: reuse shared primitives before introducing feature-local markup patterns.
- Toasts: use the wrapper from `@/components/ui`; do not import `sonner` directly in feature code.
- No new deps unless asked.
- Never add secrets or log sensitive data.
## Architecture patterns
### Thin entrypoints, focused modules
- Keep orchestration entrypoints thin: `index.js`, bridge files, bootstrap files, provider roots.
- Move route, domain, and runtime logic into focused modules with clear ownership.
- Prefer dependency injection over hidden module coupling.
- Add or update module documentation when ownership changes.
### Strong source of truth
- Prefer deterministic state over heuristics.
- Use live server/session state for live activity. Do not let historical anomalies masquerade as current execution.
- If a fallback is necessary, scope it narrowly to the active entity and treat it as temporary.
- Restore derived UI state from authoritative records. Example: restore model or agent from the latest user message, not assistant-side guesses.
### Live state vs historical state
- Derive live UI behavior from live state channels, not persisted history.
- Use historical records to restore context, not to infer that work is still in progress.
- If live state is delayed, use the narrowest possible transient fallback and clear it as soon as authoritative state arrives.
### Cross-runtime parity
- If web defines a route or payload contract that shared UI depends on, keep VS Code and desktop parity where applicable.
- Shared behavior differences must be intentional and visible in code.
- Do not ship a web-only assumption into shared UI.
### Partial-failure-safe flows
- Cross-directory and multi-entity operations must tolerate partial failure.
- Prefer per-item results, rollback paths, or resumable cleanup over all-or-nothing assumptions.
- Never leave optimistic state or local caches stranded after failure.
## CLI Parity and Safety Policy (MANDATORY)
### Principle: policy-first, UX-second
All safety and correctness rules MUST be enforced in core command logic, independent of output mode.
Interactive/pretty UX (`@clack/prompts`) is a presentation layer only.
It must never be the only place where validation or restriction is enforced.
### Required parity across modes
The same functional outcome and safety gates MUST hold for all execution modes:
- Interactive TTY (full Clack UX)
- Non-interactive shells (piped/stdin-less automation)
- `--quiet`
- `--json`
- Fully pre-specified flags (no prompts)
In all modes, invalid operations MUST fail with non-zero exit code and deterministic error semantics.
### Non-negotiable rule
Do not rely on prompts to enforce policy.
- Prompts MAY help users choose valid inputs.
- Core validators MUST run even when prompts are unavailable or skipped.
- `--quiet` suppresses non-essential output only; it does not weaken validation.
- `--json` changes output shape only; it does not weaken validation.
Detailed Clack UX patterns (primitives, prompt gating, and implementation checklist)
are defined in the `clack-cli-patterns` skill and should not be duplicated here.
## Clack CLI Skill (MANDATORY for terminal CLI work)
When working on terminal CLI commands, prompts, or output formatting, agents **MUST** study the Clack CLI skill first.
**Before starting terminal CLI work:**
```
skill({ name: "clack-cli-patterns" })
```
Scope: terminal CLI only (for example `packages/web/bin/*`). Do not apply this requirement to VS Code or web UI work.
## Theme System (MANDATORY for UI work)
When working on any UI components, styling, or visual changes, agents **MUST** study the theme system skill first.
**Before starting any UI work:**
```
skill({ name: "theme-system" })
```
This skill contains all color tokens, semantic logic, decision tree, and usage patterns. All UI colors must use theme tokens - never hardcoded values or Tailwind color classes.
## Performance rules (MANDATORY)
These rules exist because violating them has caused measurable regressions (render cascades, memory bloat, UI jank). They apply to all UI and sync layer work.
### Shared-store render discipline
- **Treat common stores as render fanout boundaries.** An unnecessary reference change in shared state can re-render large parts of the app.
- **Do not put high-frequency state in broadly consumed stores.** Fast-changing state should live in narrow stores with narrow subscribers.
- **Update only the fields that changed.** Preserve references for untouched state branches.
- **Prefer leaf selectors over container selectors.** Subscribe to the smallest stable value that satisfies the component.
- **Isolate hot consumers.** If a value changes often and only a few components need it, move it to a narrower store or consume it in a memoized child.
- **Do not subscribe shell/layout components to broad live collections.** If a shell only needs one field, entity, or derived flag, subscribe to that instead of the whole collection.
- **Treat provider roots as global hot paths.** A top-level provider must not subscribe to high-frequency data unless the feature is actually enabled and the subscription is essential.
### Zustand referential equality
Zustand skips re-renders when a selector returns the same reference (`Object.is`). Every new object/array reference triggers a re-render in every subscriber.
- **Never spread all state fields in an update.** Only create new references for fields that actually changed. A `message.part.delta` event should not clone `session`, `permission`, etc.
- **Select leaf values, not containers.** `useStore((s) => s.permission[sessionID])` is correct. `useStore((s) => s.permission)` subscribes to every permission change across all sessions.
- **Preserve references when merging.** If prepending older messages, keep existing message object references. Only add truly new items. Return the original array if nothing was added.
- **For derived collections, preserve item identity when presentation-relevant fields are unchanged.** Reuse previous item references for unchanged rows/items and move high-frequency live fields to narrow per-item selectors.
### Store splitting
A single store with N properties means every subscriber re-evaluates on every state change. Split stores by change frequency and subscriber set.
- **Group state by how often it changes.** Streaming state (updated 60/sec) must not live with user preferences (updated on click).
- **Group state by who reads it.** If only 2 components need a value, it belongs in a store that only those 2 subscribe to.
- **Cross-store reads use `.getState()`.** Actions in one store that need another store call `useOtherStore.getState()` — imperative, no subscription.
- **Never add unrelated state to an existing store** just because it's convenient. Create a new store.
### Event pipeline and SSE
- **Gate expensive operations on the hot path.** During streaming, `message.part.delta` and `message.part.updated` fire ~60/sec. Any `findIndex`, `filter`, or iteration added to these handlers multiplies across every event. Gate behind a cheap boolean check first (e.g., check `next[0]` before scanning the array).
- **Skip no-op updates.** If an incoming event doesn't change the state (same role, same finish, same timestamps), return `false` from the reducer to avoid creating new references.
- **Coalesce by key.** Same-entity events (e.g., repeated `session.status` for the same session) should replace earlier ones in the queue, not accumulate.
- **Preserve event ordering semantics.** Reducers and queues must not let stale deltas or out-of-order events corrupt the latest state.
- **Do not widen live-activity fallbacks.** A fallback for delayed status should inspect only the current trailing entity, not arbitrary historical records.
### Polling payload fidelity
- **Do not let lightweight polling erase rich fields.** If light mode omits fields (e.g., `diffStats`), preserve previous rich data until a heavy follow-up fetch lands.
- **Use two-phase polling.** Run cheap change detection first; only run heavy status fetches for directories that actually changed.
### Optimistic updates
- **Use the shadow Map pattern.** Insert optimistic data into the store for instant UI, AND register it in a separate tracking Map. Cleanup happens deterministically via `mergeOptimisticPage` on the next data fetch — not via heuristics in the event reducer.
- **Pass client-generated IDs to the server.** Use the same ID format as the server (hex-encoded timestamps). Pass `messageID` to `promptAsync` so the server echoes back the same ID. This prevents duplicates and enables in-place replacement.
- **Rollback on error.** Remove the optimistic entry from both the store and the shadow Map.
- **Stabilize bridge callbacks.** When wiring hook callbacks into module-level refs, use stable ref wrappers so effects do not loop on changing function identities.
### Session/input consistency
- **Capture send config at queue time.** Queue items must include provider/model/agent/variant snapshot; do not re-resolve from mutable live state at send time.
- **Keep server-selected attachments sendable.** Preserve server-backed file selections in queue/submit flows and convert them to proper `file://` URLs before sending.
- **Do not let text input state repaint unrelated chrome.** Typing should not force unrelated controls, menus, indicators, or toolbars to re-render on every keystroke.
- **Extract slow-changing chrome from hot input paths.** If controls do not depend on the current text value, move them behind memoized boundaries with stable callbacks.
### Bootstrap resilience
- **Treat startup 502/503 as transient.** Retry bootstrap/session-list flows with bounded retries/intervals, especially in VS Code where API readiness can lag bridge startup.
- **Use polling recovery when failures are swallowed.** If an async loader resolves without throwing on failure, recover with interval retries gated by loaded-state checks.
### Scroll and DOM
- **Never use `await waitForFrames()` for scroll preservation.** Frames of visible scroll jump are unacceptable. Use `useLayoutEffect` to adjust scroll synchronously after React commits DOM — before the browser paints.
- **Capture scroll state before the state change, restore in layout effect.** The pattern: save `scrollHeight`/`scrollTop` into a ref before triggering the update, consume it in `useLayoutEffect` on the rendered output.
- **Do not let viewport resizes masquerade as content growth.** Viewport-height changes must not trigger the same scroll compensation logic used for actual content growth.
- **Disable or narrow native/browser scroll anchoring when custom scroll logic exists.** Browser anchoring and app-managed pinning/follow logic will fight and produce jiggle.
- **Autosize textareas without transient collapse on growth.** Avoid `height='auto'` shrink/expand cycles on every character when the content only grew; this creates visible layout bounce.
### List ordering and view consistency
- **Do not sort structural lists directly from high-churn live fields.** If live updates are frequent, sorting directly from them causes reorder thrash and wide rerender cascades.
- **If live recency is required, freeze order during high-frequency updates and apply a one-shot reorder only at an intentional lifecycle edge.** Choose the lifecycle edge explicitly instead of letting every intermediate update reshuffle the UI.
- **Use one ordering source for all views of the same data.** Different views of the same entities must derive from the same ranked list or rank map; do not let each surface re-derive ordering independently.
- **Do not mix global snapshots and local live snapshots without an explicit reconciliation policy.** If multiple data sources feed one view, define which fields win and how they merge.
### Component isolation
- **Extract high-frequency hook consumers into separate components.** If a hook re-evaluates 60/sec (e.g., streaming status), wrap its consumer in a `React.memo` child component so the parent doesn't re-render.
- **Use custom `React.memo` comparators for message rows.** Compare render-relevant fields (role, finish, parts count, part IDs) — not object references.
### Caching and memory
- **Cap in-memory caches with both count and byte limits.** Entry count alone doesn't prevent memory bloat from large files. Use dual-constraint LRU (e.g., 40 entries OR 20MB).
- **Set store session limits to match loaded data.** If bootstrap loads N sessions, set `limit >= N`. Otherwise the next SSE event triggers trimming that silently removes sessions.
- **Invalidate caches on mutations.** File content cache must clear entries on write, delete, rename. Prefetch cache must clear on session eviction.
- **Use TTLs to prevent redundant fetches.** If a session was fetched <15s ago, skip re-fetching — SSE events keep it current.
### Directory context
- **Never cache directory strings in closures.** Directory can change at any time (worktree switch). Read it dynamically from `opencodeClient.getDirectory()` at call time.
- **Pass directory hints when the source of truth isn't available yet.** Newly created sessions aren't in the sync store until SSE delivers them. Pass the known directory as a parameter instead of relying on lookup.
## Regression-prevention checklist
- When adding fallback logic, ask: can stale persisted data keep this path active forever?
- When deriving UI state, ask: is this live state, historical state, or inferred state?
- When adding store fields, ask: who reads this, how often does it change, and should it live elsewhere?
- When touching polling or bootstrap, ask: can a lighter payload erase richer existing data?
- When handling optimistic updates, ask: where is rollback, reconciliation, and duplicate prevention?
- When changing shared routes or state contracts, ask: what breaks in web, desktop, and VS Code?
- When fixing a bug with a heuristic, prefer narrowing the heuristic over widening it.
## Validation expectations
- Run `bun run type-check` and `bun run lint` before finalizing.
- For hot-path changes, verify behavior under streaming or repeated events, not just static render.
- For sync or startup changes, verify fresh load, retry/failure, and restart behavior.
- For session changes, verify create, stream, abort, permission, archive/delete, and revisit flows when relevant.
## Recent changes
- Releases + high-level changes: `CHANGELOG.md`
- Recent commits: `git log --oneline` (latest tags: `v1.4.6`, `v1.4.5`)
-847
View File
@@ -1,847 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
## [Unreleased]
## [1.9.9] - 2026-04-26
- UI/Localization: added a localization foundation with translated interface strings for Spanish, Brazilian Portuguese, Ukrainian, and Simplified Chinese.
- Settings/Appearance: added selectable interface and code fonts with 10 choices each.
- Chat/Workflow: added keyboard turn navigation, widened chat content, and introduced a local workspace review and summarize slash commands for faster review handoff.
- Chat/Mobile: improved mention and autocomplete behavior with complete results, clearer active-tab scoping, and less context-switching while drafting prompts.
- Chat/Tasks: todo list progress now updates live as task status changes, and task/model status hints are steadier during active runs (thanks to @Yabuku-xD).
- Files/Editor: added an "Open files in preview mode" setting and improved multi-file edit/diff safety so review flows stay cleaner (thanks to @daveotero).
- Reliability/Performance: improved cold start and streaming responsiveness with lazy-loaded heavy components, chunk-load recovery, lower re-render churn, and safer reconnect/local-stream recovery (thanks to @Yabuku-xD, @jwcrystal, @vhqtvn).
- Desktop/Web/Mobile: improved Electron update restart behavior, PWA service-worker notifications, mobile keyboard handling, and the Add Project panel flow (thanks to @Jovines, @vhqtvn).
## [1.9.8] - 2026-04-22
- Sessions/Reliability: fixed parent-child session sync during reconnects and navigation, so status and progress stay aligned in complex session trees (thanks to @jwcrystal).
- Settings/Sync: settings updates now sync more reliably across clients, and sidebar session pagination is steadier in larger workspaces.
- Sessions/Folders: folder changes now persist through server-backed endpoints, improving consistency across environments and path setups.
- Notifications: permission notifications are now suppressed when auto-accept is enabled, reducing noise during trusted runs.
- Chat/Files: improved changed-files handling in chat and restored quick file-open flows from pending changes, so jump-to-edit stays fast (thanks to @jwcrystal).
- UI: improved bottom scroll shadow behavior and hide the tasks row when there is no active work for a cleaner conversation view.
- Reliability/Desktop: improved live event-stream recovery after transient stalls, wait briefly before failing chat actions during reconnects, and persist Electron server logs for easier disconnect debugging.
- Desktop/macOS: System color mode now tracks OS theme changes, traffic-light controls stay visible after dock restore, and update restart/changelog handling is more reliable.
- Chat/Commands: added `/summary` slash command for a non-destructive session summary - optional topic hint after the command focuses the output, and the prompt is customizable under Settings: Magic Prompts.
## [1.9.7] - 2026-04-22
- Desktop: added an Electron desktop runtime in parallel with the current Tauri app, with Electron planned to become the default path in an upcoming release.
- Plans/Notes/Todos: added editable project plans from assistant messages, external plan upload, configurable planning magic prompts, and quicker note/todo handoff into new sessions or worktrees.
- Chat/Files: you can now drag files and folders from the file tree into chat, with improved `@folder` autocomplete for faster context building (thanks to @youfch).
- Sessions/UI: added bulk session selection in the sidebar and fixed pinned sessions so they persist reliably after reloads (thanks to @yart).
- Files/Git: added a file-change summary bar and auto-refresh for open files changed outside the app, improving review flow and keeping editors in sync (thanks to @jwcrystal).
- Git/Worktrees: improved branch/worktree reliability by allowing checkout with uncommitted changes, tightening worktree cache invalidation, and reducing incorrect remote prefetches (thanks to @jwcrystal, @jasonalsing).
- Settings/MCP: improved MCP auth flow with better remote-config support and clearer diagnostics, and aligned config resolution with OpenCode behavior for more predictable setup (thanks to @daveotero, @cyan).
- Reliability/Chat: hardened bootstrap and stream-connection recovery, preserved session/connect state more reliably, and reduced streaming UI churn for smoother long runs.
- Web/PWA: added install orientation controls and fixed loopback-origin handling for web push notifications in local setups (thanks to @vhqtvn, @yart).
## [1.9.6] - 2026-04-17
- Reliability/Streaming: switched live message events to a WebSocket-first transport with SSE fallback, added response compression, and hardened proxy/compression handling so long runs stay smoother on slower or proxied networks (thanks to @geekifan, @jwcrystal).
- Sessions/Scheduled Tasks: added scheduled task creation and management with locale-aware scheduling, so recurring prompts run at the right local time without manual re-entry.
- Sessions/Worktrees: enforced session worktree isolation and tightened session-switch safety, reducing cross-worktree mix-ups when resuming chats or running Git actions (thanks to @jwcrystal).
- Files: added a full Go to Line workflow (toolbar + shortcut + dialog) and a new Copy Relative Path action, making in-editor navigation and path sharing much faster (thanks to @coldbrow).
- Files: file trees now auto-refresh when files change outside the app, so new, renamed, or updated files appear without manual reloads (thanks to @jwcrystal).
- Chat/Export: added export session as Markdown and improved empty-state/export behavior, making conversation handoff and documentation cleaner (thanks to @coldbrow).
- Chat/Requests: restored blocking request visibility in sub-sessions, scoped auto-approve to the active session tree, and reduced noisy auto-approved notifications during multi-session work.
- Desktop: added quick open and a LAN access toggle, plus safer quit behavior around scheduled tasks for smoother local-network and day-to-day desktop workflows (thanks to @An-jinu).
- Chat/Markdown: added LaTeX rendering support for clearer math and technical notation in messages (thanks to @ricautomation).
- Settings/Skills: skills are now sorted within groups so larger skill lists are easier to scan (thanks to @roctom).
## [1.9.5] - 2026-04-14
- Security/Auth: added passkey sign-in for protected instances and new 1-week/30-day session expiration options, so teams can enforce stronger access controls with flexible login persistence (thanks to @daveotero, @pm0u).
- Voice: added OpenAI-compatible custom server support for both text-to-speech and speech-to-text, including configurable TTS model/pitch/volume and stricter custom URL validation for safer setup (thanks to @ablepharus).
- Chat/Tool Output: added an interactive tree viewer for structured outputs and fixed JSON quote rendering, making large payloads easier to inspect and copy accurately (thanks to @yaozhenghangma).
- Chat/Reliability: fixed question-tool content disappearing after refresh and hardened subagent/session recovery paths, reducing silent failures and stuck task states (thanks to @jwcrystal).
- Sync/Performance: optimized multi-session streaming with per-directory queues, event coalescing, and parts-gap recovery to keep live updates smoother under heavy activity (thanks to @jwcrystal).
- Sessions/UI: kept active sessions visible in Recent, auto-expanded parent groups when opening subagent sessions, and hid empty archived/folder sections for cleaner navigation (thanks to @jwcrystal).
- Git/UI: restored Git changes panel visibility and sidebar sync, so change review stays available and consistent while switching contexts (thanks to @jwcrystal).
- Desktop/Startup: delivered a more guided first-launch and smart recovery flow, plus startup and remote-window interaction fixes to reduce early-session friction (thanks to @jwcrystal).
- Usage: added Zhipu AI Coding Plan tracking and restored model-variant compatibility with older OpenCode runtimes for more reliable quota reporting and model selection (thanks to @cainiao1992, @Chi-square-test).
## [1.9.4] - 2026-04-07
- Settings/Magic Prompts: added a dedicated Magic Prompts page with editable templates for commit/PR generation, PR and issue reviews, failed-check/comment analysis, and merge/cherry-pick conflict resolution.
- Chat/Performance: reduced streaming render churn across the app, so long responses stay smoother with less UI jitter during active runs.
- Chat/Scrolling: fixed jumpy follow behavior and restored stable bottom-resume/live-compaction updates, so staying on the latest output is more reliable.
- Reliability/Streaming: improved reconnect, retry, and directory-aware event routing to reduce stuck session/subagent states after transient disconnects (thanks to @jwcrystal, @daveotero).
- Chat/Tool Output: LSP diagnostics now render directly in tool output, making inline error review faster while iterating (thanks to @yulia-ivashko).
- Models: added defensive handling for missing model pricing/capability metadata so model controls fail less often with incomplete provider data (thanks to @Chi-square-test).
- Desktop/Performance: removed costly window translucency and reduced duplicate notification triggers for a cooler, less noisy desktop experience.
- Startup/Remote: restored remote provider startup behavior and tightened host/port detection to reduce false startup failures.
- Usage: refreshed MiniMax CN coding-plan quota data for more accurate usage reporting (thanks to @nzlov).
## [1.9.3] - 2026-03-01
- Security/Chat: user messages now escape raw HTML by default, so pasted markup is shown safely as text instead of being interpreted by the renderer (thanks to @kalac2232).
- Desktop/Performance: reduced Tauri shell CPU/GPU overhead to keep the Desktop app cooler and smoother during longer sessions.
- Sessions/Drafts: draft chat config now stays synced with the selected draft target directory, reducing wrong-model or wrong-agent carryover when switching draft context (thanks to @hkay-dev).
- VSCode/Files: added file stat support in the extension bridge so markdown-related file checks resolve more reliably before opening or rendering (thanks to @geekifan).
- Chat/Models: added arrow-key navigation for thinking-mode selection in model controls, making keyboard model tuning faster during prompt setup (thanks to @daveotero).
- Files: added HTML preview support in the file viewer, so `.html` files can be inspected visually without leaving OpenChamber (thanks to @nguyenngothuong).
- Chat: improved error message readability with clearer styling and safer word-wrapping, so failures are easier to scan without layout breakage (thanks to @nguyenngothuong).
- Chat/JSON: added an interactive JSON tree viewer with collapse/expand controls and richer color cues for easier inspection of large structured outputs (thanks to @nguyenngothuong).
- Mobile/Settings: fixed lingering settings drawers and removed extra top spacing for a cleaner, less obstructed mobile layout (thanks to @Jovines).
- Git/Worktrees: fixed worktree detection and reset stale integration state when switching contexts, reducing wrong-target behavior in worktree flows (thanks to @jwcrystal).
- Desktop/Settings: window vibrancy now correctly controls macOS window transparency, and settings copy now clarifies when full transparency changes take effect.
- Reliability/Proxy: hardened OpenCode proxy header handling (including identity-encoding normalization, compression-header cleanup, hop-by-hop response-header stripping) and suppressed expected SSE close noise, improving stream stability and reducing false proxy errors (thanks to @jwcrystal, @Jovines, @JiwaniZakir, @shekohex).
- Reliability/Proxy: restored proxied chat event streaming so live responses continue working when OpenChamber is deployed behind a proxy.
- Terminal/Reliability: switched terminal transport to a pure WebSocket path with fallback handling, improving responsiveness and stability for interactive terminal sessions (thanks to @geekifan).
- Usage/Providers: added ZhipuAI quota tracking and fixed MiniMax coding-plan and GitHub Copilot overusage calculations for more accurate usage reporting (thanks to @kalac2232, @baruchvitorino, @ebrainte).
## [1.9.2] - 2026-03-31
- Chat/Performance: rebuilt live session sync and streaming updates to cut render churn, reduce CPU spikes, and keep long-running chats smoother and more stable across runtimes.
- Worktrees/Multi-Run: added instant draft-first worktree creation and redesigned the multi-run launcher with a cleaner, faster flow for parallel runs.
- VSCode/UI: polished the extension chat and sidebar with improved spacing/tooltips, a resizable sessions pane, and better file-to-chat mention flows from Explorer.
- Models/Providers: improved custom provider model metadata loading and caching so model details stay more complete and consistent (thanks to @ZeppLu).
- CLI/Server: added `--foreground` for process-manager deployments, made managed server hostname configurable, and added an explicit `--host` option with safer localhost defaults (thanks to @colinmollenhour, @rapidrabbit76, @yulia-ivashko).
- Docker/Deployments: improved container defaults for broader compatibility, including UID 1000 user behavior, non-fatal SSH key generation, and better localhost detection in container networking (thanks to @yulia-ivashko).
- Web/PWA: fixed manifest behavior behind Cloudflare Access so install flows work more reliably in protected environments (thanks to @arthurfiorette).
## [1.9.1] - 2026-03-20
- Sessions/UI: restored Project Notes access in the sidebar, polished notes/todo editing, and fixed project action overlap so project controls stay reachable for non-git directories.
- Chat/GitHub: linked issues and pull requests now appear as user-message attachments and open more reliably across runtimes.
- Settings/MCP: adding MCP servers now consistently respects user vs project scope, preventing user-scope entries from being written into project config files.
- VSCode/Reliability: managed server startup now imports login-shell environment values and normalizes Windows workspace paths, reducing missing session/model state and proxy-related connection issues.
- Sessions: sidebar lists now keep sessions visible in both Recent and Project sections for easier discovery (thanks to @nguyenngothuong).
- Files: file trees now refresh incrementally after create/rename/delete actions, so changes appear faster without full reloads (thanks to @nguyenngothuong).
- Sessions/Worktrees: draft sessions now resolve the correct project when opened from worktree paths (thanks to @yulia-ivashko).
- Desktop: improved stale server-process cleanup on startup and fixed external link opening behavior for more predictable app interactions (thanks to @jwcrystal).
- Usage: added MiniMax Weekly quota provider support for broader usage tracking coverage (thanks to @nzlov).
## [1.9.0] - 2026-03-20
- UI/Navigation: delivered a major sidebar redesign with clearer hierarchy, unified action patterns, and improved session organization for better navigation through multiple projects (thanks to @yulia-ivashko).
- Chat: reduced streaming CPU usage and background churn with steadier turn rendering, debounced updates, and less storage thrash during long runs.
- Chat: fixed scroll-to-latest and timeline tracking behavior, so active responses stay anchored more reliably while streaming.
- Chat/Permissions: added a session-based permission auto-accept toggle and polished permission-shield visuals for quicker, clearer approval workflows.
- Git: refreshed history visuals and added clearer branch-boundary markers, improving commit review and branch context while browsing history (thanks to @yulia-ivashko).
- Git: added remote removal from sync workflows and stabilized polling to reduce noisy background refreshes (thanks to @yulia-ivashko).
- Settings/UI: fixed settings scrolling on mobile, made outside-click closing immediate, and reduced settings load churn/CPU spikes.
- Panels/UI: softened panel resize affordances and tightened service dropdown/layout spacing for a cleaner, less distracting workspace.
- Files: added debounced editor auto-save so edits persist more reliably without interrupting writing flow (thanks to @nguyenngothuong).
- Files: reworked search UI for searching in files.
- Reliability/Platform: improved Windows path/process behavior and restored macOS PTY/microphone compatibility, reducing startup/runtime friction across environments (thanks to @zerone0x, @fangfei0110).
- Desktop/macOS: lowered the minimum supported macOS version to Ventura (13.0), expanding compatibility on older systems (thanks to @craigharman).
- Updates/Reliability: unified update-check behavior across runtimes for more consistent update availability checks.
## [1.8.7] - 2026-03-13
- CLI: fixed a startup regression in global npm/bun installs where wrapper or symlinked `openchamber` entrypoints could exit without output on commands like `--version` or `status`.
- CLI: hardened entrypoint detection across direct, symlinked, and shim-based launches to keep startup behavior consistent across package managers (thanks to @shekohex).
- Windows/Web: daemon startup and Git operations no longer flash extra console windows, making background workflows less distracting (thanks to @SergioChan).
- Deployment/Docker: improved `docker run` startup behavior and entrypoint handling so containerized installs start more reliably (thanks to @nzlov).
## [1.8.6] - 2026-03-13
- Tunnel/CLI: rebuilt tunnel workflows around clearer managed modes and provider-aware lifecycle commands, with safer startup checks, improved diagnostics, and cleaner CLI output for everyday remote access (thanks to @yulia-ivashko).
- Chat: completed a turn-based rendering pipeline that keeps streaming, activity rows, and tool progress more stable in long runs, with smoother auto-follow and fewer jumpy updates.
- Chat/Settings: added richer chat render controls, including sorted/live behavior, compact live Activity previews, and options to keep Bash/Edit outputs open by default.
- Sessions/GitHub: overhauled sidebar session loading and GitHub PR tracking, and added a new minimal sidebar sessions mode on Desktop/Web, so lists stay easier to scan while PR badges and state refreshes remain accurate across active branches and remotes.
- Sessions: worktrees with active sessions now surface earlier in the sidebar, making it faster to jump back into in-progress work (thanks to @GhostFlying).
- Chat: fixed narrow-layout send behavior for modified Enter shortcuts, so keyboard sending is more reliable in compact views (thanks to @eengad).
- Chat: fixed queue-button behavior and focus-mode composer sizing, keeping input controls reachable in long prompts (thanks to @shekohex).
- Projects/Desktop: project action inputs now submit with Enter, and Desktop settings now include a spell-check toggle for writing comfort (thanks to @DocterZed).
- Mobile/PWA: install metadata now honors orientation lock more consistently, improving expected behavior on rotation-restricted devices (thanks to @atgehrhardt).
## [1.8.5] - 2026-03-04
- Desktop: startup now opens the app shell much earlier while background services continue loading, so the app feels ready faster after launch.
- Desktop/macOS: fixed early title updates that could shift traffic-light window controls on startup, keeping native controls stable in their expected position.
- VSCode: edit-style tool results now open directly in a focused diff view, so you can review generated changes at the first modified line with less manual navigation.
- VSCode: cleaned up extension settings by removing duplicate display controls and hiding sections that do not apply in the editor environment.
- Chat: fixed focus-mode composer layout so the footer action row stays pinned and accessible while writing longer prompts.
- UI/Theming: unified loading logos and startup screens across runtimes, with visuals that better match your active theme.
- Projects/UI: project icons now follow active theme foreground colors more consistently, improving readability and visual consistency in project lists.
- Reliability: improved early startup recovery so models and agents are less likely to appear missing right after launch.
- Tunnel/CLI: fixed one-time Cloudflare tunnel connect links in CLI output for `--try-cf-tunnel`, so remote collaborators can use the printed URL/QR flow successfully (thanks to @plfavreau).
- Mobile/PWA: respected OS rotation lock by removing forced orientation behavior in the web app shell (thanks to @theluckystrike).
## [1.8.4] - 2026-03-04
- Chat: added clickable file-path links in assistant messages (including line targeting), so you can jump from answer text straight to the exact file location (thanks to @yulia-ivashko).
- Chat: added a new `Changes` tool-output mode that expands edits/patches by default while keeping activity readable, making long runs easier to review (thanks to @iamhenry).
- Chat: in-progress tools now appear immediately and stay live in collapsed activity view, so active work is visible earlier with stable durations (thanks to @nelsonPires5).
- Chat: improved long user-message behavior in sticky mode with bounded height, internal scrolling, and cleaner action hit targets for better readability and control.
- Chat/Files: improved `@` file discovery and mention behavior with project-scoped search and more consistent matching, reducing wrong-project results.
- Chat/GitHub: added Attach menu actions to link GitHub issues and PRs directly in any session, making it faster to pull ticket/PR context into a prompt.
- Chat/Files: restored user image previews/fullscreen navigation and improved text-selection action placement on narrow layouts.
- Shortcuts/Models: added favorite-model cycling shortcuts, so you can switch between starred models without leaving the keyboard (thanks to @iamhenry).
- Sessions: added active-project session search in the sidebar, with clearer match behavior and easier clearing during filtering (thanks to @KJdotIO).
- Worktrees/GitHub: streamlined worktree creation with a unified flow for branches, issues, and PR-linked sessions, including cleaner validation and faster branch loading.
- Worktrees/Git: fixed branch/PR source resolution (including slash-named branches and fork PR heads), so linked worktrees track and push to the correct upstream branch.
- Git: fixed a PR panel refresh loop that could trigger repeated updates and unstable behavior in the PR section (thanks to @yulia-ivashko).
- Files/Desktop: improved `Open In` actions from file views/editors, including app selection behavior and tighter integration for opening focused files (thanks to @yulia-ivashko).
- Mobile/Projects: added long-press project editing with a bottom-sheet panel and drag-to-reorder support for faster project management on mobile (thanks to @Jovines).
- Web/PWA/Android: added improved install UX with pre-install naming and manifest shortcut updates, so installed web apps feel more customized and project-aware (thanks to @shekohex).
- UI: interactive controls now consistently show pointer cursors, improving click affordance and reducing ambiguous hover states (thanks to @KJdotIO).
- Security/Reliability: hardened terminal auth, tightened skill-file path protections, and reduced sensitive request logging exposure for safer day-to-day usage (thanks to @yulia-ivashko).
## [1.8.3] - 2026-03-02
- Chat: added user-message display controls for plain-text rendering and sticky headers, so you can tune readability to match your preferences.
- Chat/UI: overhauled the context panel with reusable tabs and embedded session chat (_beta_), making parallel context work easier without losing place.
- Chat: improved code block presentation with cleaner action alignment, restored horizontal scrolling, and polished themed highlighting across chat messages and tool output (thanks to @nelsonPires5).
- Diff: added quick open-in-editor actions from diff views that jump to the first changed line, so it is faster to move from review to edits.
- Git: refined Git sidebar tab behavior and spacing, plus bulk-revert with confirmations for easier cleanup.
- Git: fixed commit staging edge cases by filtering stale deleted paths before staging, reducing pathspec commit failures.
- Git/Worktrees: restored branch rename/edit controls in draft sessions when working in a worktree directory, so branch actions stay available earlier.
- Chat: model picker now supports collapsible provider groups and remembers expanded state between sessions.
- Settings: reorganized chat display settings into a more compact two-column layout, so more new options are easier to navigate.
- Mobile/UI: fixed session-title overflow in compact headers so running/unread indicators and actions remain visible (thanks to @iamhenry).
## [1.8.2] - 2026-03-01
- Updates: hardened the self-update flow with safer release handling and fallback behavior, reducing failed or stuck updates.
- Chat: added a new "Share as image" action so you can quickly export and share important messages (thanks to @Jovines).
- Chat: improved message readability with cleaner tool/reasoning rendering and less noisy activity timing in busy conversations (thanks to @nelsonPires5).
- Desktop/Chat: permission toasts now include session context and a clearer permission preview, making approvals more accessible outside of a session (thanks to @nelsonPires5).
- VSCode: fixed live streaming edge cases for event endpoints with query/trailing-slash variants, improving real-time updates in chat, session editor, and agent-manager views.
- Reliability: improved event-stream/session visibility handling when the app is hidden or restored, reducing stale activity states and missed updates.
- Windows: fixed CLI/runtime path and spawn edge cases to reduce startup and command failures on Windows (thanks to @plfavreau).
- Notifications/Voice: consolidated TTS and summarization service wiring for steadier text-to-speech and summary flows (thanks to @nelsonPires5).
- Deployment: fixed Docker build/runtime issues for more reliable containerized setups (thanks to @nzlov).
## [1.8.1] - 2026-02-28
- Web/Auth: fixed an issue where non-tunnel browser sessions could incorrectly show a tunnel-only lock screen; normal auth flow now appears unless a tunnel is actually active.
## [1.8.0] - 2026-02-28
- Desktop: added SSH remote instance support with dedicated lifecycle and UX flows, so you can work against remote machines more reliably (thanks to @shekohex).
- Projects: added project icon customization with upload/remove and automatic favicon discovery from your repository (thanks to @shekohex).
- Projects: added header project actions on Web and Mobile, so you can run and stop any configured project commands without leaving chat.
- Projects/Desktop: project actions can also open SSH-forwarded URLs, making remote dev-server workflows quicker from inside the app.
- Desktop: added dynamic window titles that reflect active project and remote context, so it is easier to track where you are working (thanks to @shekohex).
- Remote Tunnel: added tunnel settings with quick/named modes, secure one-time connect links (with QR), and saved named-tunnel presets/tokens so enabling remote access is easier and safer (thanks to @yulia-ivashko).
- UI: expanded sprite-based file and folder icons across Files, Diff, and Git views for faster visual scanning (thanks to @shekohex).
- UI: added an expandable project rail with project names, a settings toggle, and saved expansion state for easier navigation in multi-project setups (thanks to @nguyenngothuong).
- UI/Files: added file-type icons across file lists, tabs, and diffs, so you can identify files faster at a glance (thanks to @shekohex).
- Files: added a read-only highlighted view with a quick toggle back to edit mode, so you can quickly review code with richer syntax rendering if you don't need to edit thing (thanks to @shekohex).
- Files: markdown preview now handles frontmatter more cleanly, improving readability for docs-heavy repos (thanks to @shekohex).
- Chat: improved long-session performance with virtualized message rendering, smoother scrolling, and more stable behavior in large histories (thanks to @shekohex).
- Chat: enabled markdown rendering in user messages for clearer formatted prompts and notes (thanks to @haofeng0705).
- Chat: enabled bueatiful diffs for edit tools in chat making this aligned with dedicated diffs view style (thanks to @shekohex).
- Chat: pasted absolute paths are now treated as normal messages, reducing accidental command-like behavior when sharing paths.
- Chat: fixed queued sends for inactive sessions, reducing stuck queues.
- Chat: upgraded Mermaid rendering with a cleaner diagram view plus quick copy/download actions, making generated diagrams easier to read and share (thanks to @shekohex).
- Notifications: improved child-session notification detection to reduce missed or misclassified subtask updates (thanks to @Jovines).
- Deployment: added Docker deployment support with safer container defaults and terminal shell fallback, making self-hosted setups easier to run (thanks to @nzlov).
- Reliability: improved Windows compatibility across git status checks, OpenCode startup, path normalization, and session merge behavior (thanks to @mmereu).
- Usage: added MiniMax coding-plan quota provider support for broader usage tracking coverage (thanks to @nzlov).
- Usage: added Ollama Cloud quota provider support for broader usage tracking coverage (thanks to @iamhenry).
## [1.7.5] - 2026-02-25
- UI: moved projects into a dedicated sidebar rail and tightened the layout so switching projects and sessions feels faster.
- Chat: fixed an issue where messages could occasionally duplicate or disappear during active conversations.
- Sessions: reduced session-switching overhead to make chat context changes feel more immediate.
- Reliability/Auth: migrated session auth storage to signed JWTs with a persistent secret, reducing unexpected auth-state drift after reconnects or reloads (thanks to @Jovines).
- Mobile: pending permission prompts now recover after reconnect/resume instead of getting lost mid-run (thanks to @nelsonPires5).
- Mobile/Chat: refined message spacing and removed the top scroll shadow for a cleaner small-screen reading experience (thanks to @Jovines).
- Web: added `OPENCODE_HOST` support so you can connect directly to an external OpenCode server using a full base URL (thanks to @colinmollenhour).
- Web/Mobile: fixed in-app update flow in containerized setups so updates apply correctly.
## [1.7.4] - 2026-02-24
- Settings: redesigned the settings workspace with flatter, more consistent page layouts so configuration is faster to scan and edit.
- Settings: improved agents and skills navigation by grouping entries by subfolder for easier management at scale (thanks to @nguyenngothuong).
- Chat: improved streaming smoothness and stability with buffered updates and runtime fixes, reducing lag, stuck spinners, memory growth, and timeout-related interruptions in long runs (thanks to @nguyenngothuong).
- Chat: added fullscreen Mermaid preview, persisted default thinking variant selection, and hardened file-preview safety checks for a safer, more predictable message experience (thanks to @yulia-ivashko).
- Chat: draft text now persists per session, and the input supports an expanded focus mode for longer prompts (thanks to @nguyenngothuong).
- Sessions: expanded folder management with subfolders, cleaner organization actions, and clearer delete confirmations (thanks to @nguyenngothuong).
- Settings: added an MCP config manager UI to simplify editing and validating MCP server configuration (thanks to @nguyenngothuong).
- Git/PR: moved commit-message and PR-description generation to active-session structured output, so generation uses current session context and avoids fragile backend polling.
- Chat Activity: improved Structured Output tool rendering with dedicated title/icon, clearer result descriptions, and more reliable detailed expansion defaults.
- Notifications/Voice: moved utility model controls into AI Summarization as a Zen-only Summarization Model setting.
- Mobile: refreshed drawer and session-status layouts for better small-screen usability (thanks to @Jovines).
- Desktop: improved remote instance URL handling for more reliable host/query matching (thanks to @shekohex).
- Files: added C, C++, and Go language support for syntax-aware rendering in code-heavy workflows (thanks to @fomenks).
## [1.7.3] - 2026-02-21
- Settings: added customizable keyboard shortcuts for chat actions, panel toggles, and services, so you can better match OpenChamber to your workflow (thanks to @nelsonPires5).
- Sessions: added custom folders to group chat sessions, with move/rename/delete flows and persisted collapse state per project (thanks to @nguyenngothuong).
- Notifications: improved agent progress notifications and permission handling to reduce noisy prompts during active runs (thanks to @nguyenngothuong).
- Diff/Plans/Files: restored inline comments making more like a GitHub style again (thanks to @nelsonPires5).
- Terminal: restored terminal text copy behavior, so selecting and copying command output works reliably again (thanks to @shekohex).
- UI: unified clipboard copy behavior across Desktop app, Web app, and VS Code extension for more consistent copy actions and feedback.
- Reliability: improved startup environment detection by capturing login-shell environment snapshots, reducing missing PATH/tool issues on launch.
- Reliability: refactored OpenCode config/auth integration into domain modules for steadier provider auth and command loading flows (thanks to @nelsonPires5).
## [1.7.2] - 2026-02-20
- Chat: question prompts now guide you to unanswered items before submit, making tool-question flows faster.
- Chat: fixed auto-send queue to wait for the active session to be idle before sending, reducing misfires during agent messages.
- Chat: improved streaming activity rendering and session attention indicators, so active progress and unread signals stay more consistent.
- UI: added Plan view in the context sidebar panel for quicker access to plan content while you work (thanks to @nelsonPires5).
- Settings: model variant options now refresh correctly in draft/new-session flows, avoiding stale selections.
- Reliability: provider auth failures now show clearer re-auth guidance when tokens expire, making recovery faster (thanks to @yulia-ivashko).
## [1.7.1] - 2026-02-18
- Chat: slash commands now follow server command semantics (including multiline arguments), so command behavior is more consistent with OpenCode CLI.
- Chat: added a shell mode triggered by leading `!`, with inline output visibility/copy.
- Chat: improved delegated-task clarity with richer subtask bubbles, better task-detail rendering, and parent-chat surfacing for child permission/question requests.
- Chat: improved `@` mention autocomplete by prioritizing agents and cleaning up ordering for faster picks.
- Skills: discovery now uses OpenCode API as the source of truth with safer fallback scanning, improving installed-state accuracy.
- Skills: upgraded editing/install UX with better code editing, syntax-aware related files, and clearer location targeting across user/project .opencode and .agents scopes.
- Mobile: fixed accidental abort right after tapping Send on touch devices, reducing interrupted responses (thanks to @shekohex).
- Maintenance: removed deprecated GitHub Actions cloud runtime assets and docs to reduce setup confusion (thanks to @yulia-ivashko).
## [1.7.0] - 2026-02-17
- Chat: improved live streaming with part-delta updates and smarter auto-follow scrolling, so long responses stay readable while they generate.
- Chat: Mermaid diagrams now render inline in assistant messages, with quick copy/download actions for easier sharing.
- UI: added a context overview panel with token usage, cost breakdown, and raw message inspection to make session debugging easier.
- Sessions: project icon and color customizations now persist reliably across restarts.
**- Reliability: managed local OpenCode runtimes now use rotated secure auth and tighter lifecycle control across runtimes, reducing stale-process and reconnect issues (thanks to @yulia-ivashko).**
- Git/GitHub: improved backend reliability for repository and auth operations, helping branch and PR flows stay more predictable (thanks to @nelsonPires5).
## [1.6.9] - 2026-02-16
- **UI: redesigned the workspace shell with a context panel, tabbed sidebars, and quicker navigation across chat, files, and reviews, so daily workflows feel more focused.**
- UI: compact model info in selection (price + capabilities), making model selection faster and more cost-aware (thanks to @nelsonPires5).
- Chat: fixed files attachment issue and added displaying of excided quota information.
- Diff: improved large diff rendering and interaction performance for smoother reviews on heavy changesets.
- Worktrees: shipped an upstream-first flow across supported runtimes, making branch tracking and worktree session setup more predictable (thanks to @yulia-ivashko).
- Git: improved pull request branch normalization and base/remote resolution to reduce PR setup mismatches (thanks to @gsxdsm).
- Sessions: added a persistent project notes and todos panel, so key context and follow-ups stay attached to each project (thanks to @gsxdsm).
- Sessions: introduced the ability to pin sessions within your groups for easy access.
- Settings: added a configurable Zen model for commit messages generation and summarization of notifications (thanks to @gsxdsm).
- Usage: added NanoGPT quota support and hardened provider handling for more reliable usage tracking (thanks to @nelsonPires5).
- Reliability: startup now auto-detects and safely connects to an existing OpenCode server, reducing duplicate-server conflicts (thanks to @ruslan-kurchenko).
- Desktop: improved day-to-day polish with restored desktop window geometry and posiotion (thanks to @yulia-ivashko).
- Mobile: fixes for small-screen editor, terminal, and layout overlap issues (thanks to @gsxdsm, @nelsonPires5).
## [1.6.8] - 2026-02-12
- Chat: added drag-and-drop attachments with inline image previews, so sharing screenshots and files in prompts feels much faster and more reliable.
- Sessions: fixed a sidebar issue where draft input could carry over when switching projects, so each workspace keeps cleaner chat context.
- Chat: improved quick navigation from the sessions list by adding double-click to jump into chat and auto-focus the draft input; also fixed mobile session return behavior (thanks to @gsxdsm).
- Chat: improved agent/model picking with fuzzy search across names and descriptions, making long lists easier to filter.
- Usage: corrected Gemini and Antigravity quota source mapping and labels for more accurate usage tracking (thanks to @gsxdsm).
- Usage: when using remaining-quota mode, usage markers now invert direction to better match how remaining capacity is interpreted (thanks to @gsxdsm).
- Desktop: fixed project selection in opened remote instances.
- Desktop: fixed opened remote instances that use HTTP (helpful for instances under tunneling).
## [1.6.7] - 2026-02-10
- Voice: added built-in voice input and read-aloud responses with multiple providers, so you can drive chats hands-free when typing is slower (thanks to @gsxdsm).
- Git: added multi-remote push selection and smarter fork-aware pull request creation to reduce manual branch/remote setup (thanks to @gsxdsm).
- Usage: added usage pace and prediction indicators in the header and settings, so it is easier to see how quickly quota is moving (thanks to @gsxdsm).
- Diff/Plans: fixed comment draft collisions and improved multi-line comment editing in plan and file workflows, so feedback is less likely to get lost (thanks to @nelsonPires5).
- Notifications: stopped firing completion notifications for comment draft edits to reduce noisy alerts during review-heavy sessions (thanks to @nelsonPires5).
- Settings: added confirmation dialogs for destructive delete/reset actions to prevent accidental data loss.
- UI: refreshed header and settings layout, improved host switching, and upgraded the editor for smoother day-to-day navigation and editing.
- Desktop: added multi-window support with a dedicated "New Window" action for parallel work across projects (thanks to @yulia-ivashko).
- Reliability: fixed message loading edge cases, stabilized voice-mode persistence across restarts, and improved update flow behavior across platforms.
## [1.6.6] - 2026-02-9
- Desktop: redesigned the main workspace with a dedicated Git sidebar and bottom terminal dock, so Git and terminal actions stay in reach while chatting.
- Desktop: added an `Open In` button to open the current workspace in Finder, Terminal, and supported editors with remembered app preference (thanks to @yulia-ivashko).
- Header: combined Instance, Usage, and MCP into one services menu for faster access to runtime controls and rate limits while decluttering the header space.
- Git: added push/pull with remote selection, plus in-app rebase/merge flows with improved remote inference and clearer conflict handling (thanks to @gsxdsm).
- Git: reorganized the Git workspace with improved in-app PR workflows.
- Files: improved editing with breadcrumbs, better draft handling, smoother editor interactions, and more reliable directory navigation from file context (thanks to @nelsonPires5).
- Sessions: improved status behavior, faster mobile session switching with running/unread indicators, and clearer worktree labels when branch name differs (thanks to @Jovines, @gsxdsm).
- Notifications: added smarter templates with concise summaries, so completion alerts are easier to scan (thanks to @gsxdsm).
- Usage: added per-model quota breakdowns with collapsible groups, and fixed provider dropdown scrolling (thanks to @nelsonPires5, @gsxdsm).
- Terminal: improved input responsiveness with a persistent low-latency transport for steadier typing (thanks to @shekohex).
- Mobile: fixed chat input layout issues on small screens (thanks to @nelsonPires5).
- Reliability: fixed OpenCode auth pass-through and proxy env handling to reduce intermittent connection/auth issues (thanks to @gsxdsm).
## [1.6.5] - 2026-02-6
- Settings: added an OpenCode CLI path override so you can point OpenChamber at a custom/local CLI install.
- Chat: added arrow-key prompt history and an optional setting to persist input drafts between restarts (thanks to @gsxdsm).
- Chat: thinking/reasoning blocks now render more consistently, and justification visibility settings now apply reliably (thanks to @gsxdsm).
- Diff/Plans: added inline comment drafts so you can leave line-level notes and feed them back into requests (thanks to @nelsonPires5).
- Sessions: you can now rename projects directly from the sidebar, and issue/PR pickers are easier to scan when starting from GitHub context (thanks to @shekohex, @gsxdsm).
- Worktrees: improved worktree flow reliability, including cleaner handling when a worktree was already removed outside the app (thanks to @gsxdsm).
- Terminal: improved Android keyboard behavior and removed distracting native caret blink in terminal inputs (thanks to @shekohex).
- UI: added Vitesse Dark and Vitesse Light theme presets.
- Reliability: improved OpenCode binary resolution and HOME-path handling across runtimes for steadier local startup.
## [1.6.4] - 2026-02-5
- Desktop: switch between local and remote OpenChamber instances, plus a thinner runtime for better feature parity and fewer desktop-only quirks.
- VSCode: improved Windows PATH resolution and cold-start readiness checks to reduce "stuck loading" for sessions/models/agents.
- Mobile: split Agent/Model controls and a quick commands button with autocomplete (Commands/Agents/Files) for easier input (thanks to @Jovines, @gsxdsm).
- Chat: select text in messages to quickly add it to your prompt or start a new session (thanks to @gsxdsm).
- Diff/Plans: add inline comment drafts so you can annotate specific lines and include those notes in requests (thanks to @nelsonPires5).
- Terminal/Syntax: font size controls and Phoenix file extension support for better highlighting in files and diffs (thanks to @shekohex).
- Usage: expanded quota tracking with more providers (including GitHub Copilot) and a provider selector dropdown (thanks to @gsxdsm, @nelsonPires5).
- Git: improved macOS SSH agent support for smoother private-repo auth (thanks to @shekohex).
- Web: fixed missing icon when installing the Android PWA (thanks to @nelsonPires5).
- GitHub: PR description generation supports optional extra context for better summaries (thanks to @nelsonPires5).
## [1.6.3] - 2026-02-2
- Web: improved server readiness check to use the `/global/health` endpoint for more reliable startup detection.
- Web: added login rate limit protection to prevent brute-force attempts on the authentication endpoint (thanks to @Jovines).
- VSCode: improved server health check with the proper health API endpoint and increased timeout for steadier startup (thanks to @wienans).
- Settings: dialog no longer persists open/closed state across app restarts.
## [1.6.2] - 2026-02-1
- Usage: new multi-provider quota dashboard to monitor API usage across OpenAI, Google, and z.ai (thanks to @nelsonPires5).
- Settings: now opens in a windowed dialog on desktop with backdrop blur for better focus.
- Terminal: added tabbed interface to manage multiple terminal sessions per directory.
- Files: added multi-file tabs on desktop and dropdown selector on mobile (thanks to @nelsonPires5).
- UI: introduced token-based theming system and 18 themes with light/dark variants; with support for custom user themes from `~/.config/openchamber/themes`.
- Diff: optimized stacked view with worker-pool processing and lazy DOM rendering for smoother scrolling.
- Worktrees: workspace path now resolves correctly when using git worktrees (thanks to @nelsonPires5).
- Projects: fixed directory creation outside workspace in the Add Project modal (thanks to @nelsonPires5).
## [1.6.1] - 2026-01-30
- Chat: added Stop button to cancel generation mid-response.
- Mobile: revamped chat controls on small screens with a unified controls drawer (thanks to @nelsonPires5).
- UI: update dialog now includes the changelog so you can review what's new before updating.
- Terminal: added optional on-screen key bar (Esc/Ctrl/arrows/Enter) for easier terminal navigation.
- Notifications: added "Notify for subtasks" toggle to silence child-session notifications during multi-run (thanks to @Jovines).
- Reliability: improved event-stream reconnection when the app becomes visible again.
- Worktrees: starting new worktree sessions now defaults to HEAD when no start point is provided.
- Git: commit message generation now includes untracked files and handles git diff --no-index comparisons more reliably (thanks to @MrLYC).
- Desktop: improved macOS window chrome and header spacing, including steadier traffic lights on older macOS versions (thanks to @yulia-ivashko).
## [1.6.0] - 2026-01-29
- Chat: added message stall detection with automatic soft resync for more reliable message delivery.
- Chat: fixed "Load older" button behavior in chat with proper pagination implementation.
- Git: PR picker now validates local branch existence and includes a refresh action.
- Git: worktree integration now syncs clean target directories before merging.
- Diff: fixed memory leak when viewing many modified files; large changesets now lazy-load for smoother performance.
- VSCode: session activity status now updates reliably even when the webview is hidden.
- Web: session activity tracking now works consistently across browser tabs.
- Reliability: plans directory no longer errors when missing.
## [1.5.9] - 2026-01-28
- Worktrees: migrated to Opencode SDK worktree implementation; sessions in worktrees are now completely isolated.
- Git: integrate worktree commits back to a target branch with commit previews and guided conflict handling.
- Files: toggle markdown preview when viewing files (thanks to @Jovines).
- Files: open the file viewer in fullscreen for focused review and editing (thanks to @TaylorBeeston).
- Plans: switch between markdown preview and edit mode in the Plan view.
- UI: Files, Diff, Git, and Terminal now follow the active session/worktree directory, including new-session drafts.
- Web: plan lists no longer error when the plans directory is missing.
## [1.5.8] - 2026-01-26
- Plans: new Plan/Build mode switching support with dedicated Plan content view with per-session context.
- GitHub: sign in with multiple accounts and smoother auth flow.
- Chat/UI: linkable mentions, better wrapping, and markdown/scroll polish in messages.
- Skills: ClawdHub catalog now pages results and retries transient failures.
- Diff: fixed Chrome scrolling in All Files layout.
- Mobile: improved layout for attachments, git, and permissions on small screens (thanks to @nelsonPires5).
- Web: iOS safe-area support for the PWA header.
- Activity: added a text-justification setting for activity summaries (thanks to @iyangdianfeng).
- Reliability: file lists and message sends handle missing directories and transient errors more gracefully.
## [1.5.7] - 2026-01-24
- GitHub: PR panel supports fork PR detection by branch name.
- GitHub: Git tab PR panel can send failed checks/comments to chat with hidden context; added check details dialog with Actions step breakdown.
- Web: GitHub auth flow fixes.
## [1.5.6] - 2026-01-24
- GitHub: connect your account in Settings with device-flow auth to enable GitHub tools.
- Sessions: start new sessions from GitHub issues with seeded context (title, body, labels, comments).
- Sessions: start new sessions from GitHub pull requests with PR context baked in (including diffs).
- Git: manage pull requests in the Git view with AI-generated descriptions, status checks, ready-for-review, and merge actions.
- Mobile: fixed CommandAutocomplete dropdown scrolling (thanks to @nelsonPires5).
## [1.5.5] - 2026-01-23
- Navigation: URLs now sync the active session, tab, settings, and diff state for shareable links and reliable back/forward (thanks to @TaylorBeeston).
- Settings: agent and command overrides now prefer plural directories while still honoring legacy singular folders.
- Skills: installs now target plural directories while still recognizing legacy singular folders.
- Web: push notifications no longer fire when a window is visible, avoiding duplicate alerts.
- Web: improved push subscription handling across multiple windows for more reliable delivery.
## [1.5.4] - 2026-01-22
- Chat: new Apply Patch tool UI with diff preview for patch-based edits.
- Files: refreshed attachment cards and related file views for clearer context.
- Settings: manage provider configuration files directly from the UI.
- UI: updated header and sidebar layout for a cleaner, tighter workspace fit (thanks to @TheRealAshik).
- Diff: large diffs now lazy-load to avoid freezes (thanks to @Jovines).
- Web: added Background notifications for PWA.
- Reliability: connect to external OpenCode servers without auto-start and fixed subagent crashes (thanks to @TaylorBeeston).
## [1.5.3] - 2026-01-20
- Files: edit files inline with syntax highlighting, draft protection, and save/discard flow.
- Files: toggles to show hidden/dotfiles and gitignored entries in file browsers and pickers (thanks to @syntext).
- Settings: new memory limits controls for session message history.
- Chat: smoother session switching with more stable scroll anchoring.
- Chat: new Activity view in collapsed state, now shows latest 6 tools by default.
- Chat: fixed message copy on Firefox for macOS (thanks to @syntext).
- Appearance: new corner radius control and restored input bar offset setting (thanks to @TheRealAshik).
- Git: generated commit messages now auto-pick a gitmoji when enabled (thanks to @TheRealAshik).
- Performance: faster filesystem/search operations and general stability improvements (thanks to @TheRealAshik).
## [1.5.2] - 2026-01-17
- Sessions: added branch picker dialog to start new worktree sessions from local branches (thanks to @nilskroe).
- Sessions: added project header worktree button, active-session loader, and right-click context menu in the sessions sidebar (thanks to @nilskroe).
- Sessions: improved worktree delete dialog with linked session details, dirty-change warnings, and optional remote branch removal.
- Git: added gitmoji picker in commit message composer with cached emoji list (thanks to @TaylorBeeston).
- Chat: optimized message loading for opening sessions.
- UI: added one-click diagnostics copy in the About dialog.
- VSCode: tuned layout breakpoint and server readiness timeout for steadier startup.
- Reliability: improved OpenCode process cleanup to reduce orphaned servers.
## [1.5.1] - 2026-01-16
- Desktop: fixed orphaned OpenCode processes not being cleaned up on restart or exit.
- Opencode: fixed issue with reloading configuration was killing the app
## [1.5.0] - 2026-01-16
- UI: added a new Files tab to browse workspace files directly from the interface.
- Diff: enhanced the diff viewer with mobile support and the ability to ask the agent for comments on changes.
- Git Identities: added "default identity" setting with one-click set/unset and automatic local identity detection.
- VSCode: improved server management to ensure it initializes within the workspace directory with context-aware readiness checks.
- VSCode: added responsive layout with sessions sidebar + chat side-by-side when wide, compact header, and streamlined settings.
- Web/VSCode: fixed orphaned OpenCode processes not being cleaned up on restart or exit.
- Web: the server now automatically resolves and uses an available port if the default is occupied.
- Stability: fixed heartbeat race condition causing session stalls during long tasks (thanks to @tybradle).
- Desktop: fixed commands for worktree setup access to PATH.
## [1.4.9] - 2026-01-14
- VSCode: added session editor panel to view sessions alongside files.
- VSCode: improved server connection reliability with multiple URL candidate support.
- Diff: added stacked/inline diff mode toggle in settings with sidebar file navigation (thanks to @nelsonPires5).
- Mobile: fixed iOS keyboard safe area padding for home indicator bar (thanks to @Jovines).
- Upload: increased attachment size limit to 50MB with automatic image compression to 2048px for large files.
## [1.4.8] - 2026-01-14
- Git Identities: added token-based authentication support with ~/.git-credentials discovery and import.
- Settings: consolidated Git settings and added opencode zen model selection for commit generation (thanks to @nelsonPires5).
- Web Notifications: added configurable native web notifications for assistant completion (thanks to @vio1ator).
- Chat: sidebar sessions are now automatically sorted by last updated date (thanks to @vio1ator).
- Chat: fixed edit tool output and added turn duration.
- UI: todo lists and status indicators now hide automatically when all tasks are completed (thanks to @vio1ator).
- Reliability: improved project state preservation on validation failures (thanks to @vio1ator) and refined server health monitoring.
- Stability: added graceful shutdown handling for the server process (thanks to @vio1ator).
## [1.4.7] - 2026-01-10
- Skills: added ClawdHub integration as built-in market for skills.
- Web: fixed issues in terminal
## [1.4.6] - 2026-01-09
- VSCode/Web: switch opencode cli management to SDK.
- Input: removed auto-complete and auto-correction.
- Shortcuts: switched agent cycling shortcut from Shift + TAB to TAB again.
- Chat: added question tool support with a rich UI for interaction.
## [1.4.5] - 2026-01-08
- Chat: added support for model variants (thinking effort).
- Shortcuts: Switched agent cycling shortcut from TAB to Shift + TAB.
- Skills: added autocomplete for skills on "/" when it is not the first character in input.
- Autocomplete: added scope badges for commands/agents/skills.
- Compact: changed /summarize command to be /compact and use sdk for compaction.
- MCP: added ability to dynamically enabled/disabled configured MCP.
- Web: refactored project adding UI with autocomplete.
## [1.4.4] - 2026-01-08
- Agent Manager / Multi Run: select agent per worktree session (thanks to @wienans).
- Agent Manager / Multi Run: worktree actions to delete group or individual worktrees, or keep only selected one (thanks to @wienans).
- Agent Manager: added "Copy Worktree Path" action in the more menu (thanks to @wienans).
- Worktrees: added session creation flow with loading screen, auto-create worktree setting, and setup commands management.
- Session sidebar: refactoring with unified view for sessions in worktrees.
- Settings: added ability to create new session in worktree by default
- Git view: added branch rename for worktree.
- Chat: fixed IME composition for CJK input to prevent accidental send (thanks to @madebyjun).
- Projects: added multi-project support with per-project settings for agents/commands/skills.
- Event stream: improved SSE with heartbeat management, permission bootstrap on connect, and reconnection logic.
- Tunnel: added QR code and password URL for Cloudflare tunnel (thanks to @martindonadieu).
- Model selector: fixed dropdowns not responding to viewport size.
## [1.4.3] - 2026-01-04
- VS Code extension: added Agent Manager panel to run the same prompt across up to 5 models in parallel (thanks to @wienans).
- Added permission prompt UI for tools configured with "ask" in opencode.json, showing requested patterns and "Always Allow" options (thanks to @aptdnfapt).
- Added "Open subAgent session" button on task tool outputs to quickly navigate to child sessions (thanks to @aptdnfapt).
- VS Code extension: improved activation reliability and error handling.
## [1.4.2] - 2026-01-02
- Added timeline dialog (`/timeline` command or Cmd/Ctrl+T) for navigating, reverting, and forking from any point in the conversation (thanks to @aptdnfapt).
- Added `/undo` and `/redo` commands for reverting and restoring messages in a session (thanks to @aptdnfapt).
- Added fork button on user messages to create a new session from any point (thanks to @aptdnfapt).
- Desktop app: keyboard shortcuts now use Cmd on macOS and Ctrl on web/other platforms (thanks to @sakhnyuk).
- Migrated to OpenCode SDK v2 with improved API types and streaming.
## [1.4.1] - 2026-01-02
- Added the ability to select the same model multiple times in multi-agent runs for response comparison.
- Model selector now includes search and keyboard navigation for faster model selection.
- Added revert button to all user messages (including first one).
- Added HEIC image support for file attachments with automatic MIME type normalization for text format files.
- VS Code extension: added git backend integration for UI to access (thanks to @wienans).
- VS Code extension: Only show the main Worktree in the Chat Sidebar (thanks to @wienans).
- Web app: terminal backend now supports a faster Bun-based PTY when Bun is available, with automatic fallback for existing Node-only setups.
- Terminal: improved terminal performance and stability by switching to the Ghostty-based terminal renderer, while keeping the existing terminal UX and per-directory sessions.
- Terminal: fixed several issues with terminal session restore and rendering under heavy output, including switching directories and long-running TUI apps.
## [1.4.0] - 2026-01-01
- Added the ability to run multiple agents from a single prompt, with each agent working in an isolated worktree.
- Git view: improved branch publishing by detecting unpublished commits and automatically setting the upstream on first push.
- Worktrees: new branch creation can start from a chosen base; remote branches are only created when you push.
- VS Code extension: default location is now the right secondary sidebar in VS Code, and the left activity bar in Cursor/Windsurf; navigation moved into the title bar (thanks to @wienans).
- Web app: added Cloudflare Quick Tunnel support for simpler remote access (thanks to @wojons and @aptdnfapt).
- Mobile: improved keyboard/input bar behavior (including Android fixes and better keyboard avoidance) and added an offset setting for curved-screen devices (thanks to @auroraflux).
- Chat: now shows clearer error messages when agent messages fail.
- Sidebar: improved readability for sticky headers with a dynamic background.
## [1.3.9] - 2025-12-30
- Added skills management to settings with the ability to create, edit, and delete skills (make sure you have the latest OpenCode version for skills support).
- Added Skills catalog functionality for discovering and installing skills from external sources.
- VS Code extension: added right-click context menu with "Add to Context," "Explain," and "Improve Code" actions (thanks to @wienans).
## [1.3.8] - 2025-12-29
- Added Intel Mac (x86_64) support for the desktop application (thanks to @rothnic).
- Build workflow now generates separate builds for Apple Silicon (arm64) and Intel (x86_64) Macs (thanks to @rothnic).
- Improved dev server HMR by reusing a healthy OpenCode process to avoid zombie instances.
- Added queued message mode with chips, batching, and idle autosend (including attachments).
- Added queue mode toggle to OpenChamber settings (chat section) with persistence across runtimes.
- Fixed scroll position persistence for active conversation turns across session switches.
- Refactored Agents/Commands management with ability to configure project/user scopes.
## [1.3.7] - 2025-12-28
- Redesigned Settings as a full-screen view with tabbed navigation.
- Added mobile-friendly drill-down navigation for settings.
- ESC key now closes settings; double-ESC abort only works on chat tab without overlays.
- Added responsive tab labels in settings header (icons only at narrow widths).
- Improved session activity status handling and message step completion logic.
- Introduced enchanced VSCode extension settings with dynamic layout based on width.
## [1.3.6] - 2025-12-27
- Added the ability to manage (connect/disconnect) providers in settings.
- Adjusted auto-summarization visuals in chat.
## [1.3.5] - 2025-12-26
- Added Nushell support for operations with Opencode CLI.
- Improved file search with fuzzy matching capabilities.
- Enhanced mobile responsiveness in chat controls.
- Fixed workspace switching performance and API health checks.
- Improved provider loading reliability during workspace switching.
- Fixed session handling for non-existent worktree directories.
- Added Discord links in the about section.
- Added settings for choosing the default model/agent to start with in a new session.
## [1.3.4] - 2025-12-25
- Diff view now loads reliably even with large files and slow networks.
- Fixed getting diffs for worktree files.
- VS Code extension: improved type checking and editor integration.
## [1.3.3] - 2025-12-25
- Updated OpenCode SDK to 1.0.185 across all app versions.
- VS Code extension: fixed startup, more reliable OpenCode CLI/API management, and stabilized API proxying/streaming.
- VS Code extension: added an animated loading screen and introduced command for status/debug output.
- Fixed session activity tracking so it correctly handles transitions through states (including worktree sessions).
- Fixed directory path handling (including `~` expansion) to prevent invalid paths and related Git/worktree errors.
- Chat UI: improved turn grouping/activity rendering and fixed message metadata/agent selection propagation.
- Chat UI: improved agent activity status behavior and reduced image thumbnail sizes for better readability.
## [1.3.2] - 2025-12-22
- Fixed new bug session when switching directories
- Updated Opencode SDK to the latest version
## [1.3.1] - 2025-12-22
- New chats no longer create a session until you send your first message.
- The app opens to a new chat by default.
- Fixed mobile and VSCode sessions handling
- Updated app identity with new logo and icons across all platforms.
## [1.3.0] - 2025-12-21
- Added revert functionality in chat for user messages.
- Polished mobile controls in chat view.
- Updated user message layout/styling.
- Improved header tab responsiveness.
- Fixed bugs with new session creation when the VSCode extension initialized for the first time.
- Adjusted VSCode extension theme mapping and model selection view.
- Polished file autocomplete experience.
## [1.2.9] - 2025-12-20
- Session autocleanup feature with configurable retention for each app version including VSCode extension.
- Ability to update web package from mobile/PWA view in setting.
- A lot of different optimization for a long sessions.
## [1.2.8] - 2025-12-19
- Introduced update mechanism for web version that doesn't need any cli interaction.
- Added installation script for web version with package managed detection.
- Update and restart of web server now support automatic pick-up of previously set parameters like port or password.
## [1.2.7] - 2025-12-19
- Comprehensive macOS native menu bar entries.
- Redesigned directory selection view for web/mobile with improved layout.
- Improved theme consistency across dropdown menus, selects, and command palette.
- Introduced keyboard shortcuts help menu and quick actions menu.
## [1.2.6] - 2025-12-19
- Added write/create tool preview in permission cards with syntax highlighting.
- More descriptive assistant status messages with tool-specific and varied idle phrases.
- Polished Git view layout
## [1.2.5] - 2025-12-19
- Polished chat expirience for longer session.
- Fixed file link from git view to diff.
- Enhancements to the inactive state management of the desktop app.
- Redesigned Git tab layout with improved organization.
- Fixed untracked files in new directories not showing individually.
- Smoother session rename experience.
## [1.2.4] - 2025-12-18
- MacOS app menu entries for Check for update and for creating bug/request in Help section.
- For Mobile added settings, improved terminal scrolling, fixed app layout positioning.
## [1.2.3] - 2025-12-17
- Added image preview support in Diff tab (shows original/modified images instead of base64 code).
- Improved diff view visuals and alligned style among different widgets.
- Optimized git polling and background diff+syntax pre-warm for instant Diff tab open.
- Optomized reloading unaffected diffs.
## [1.2.2] - 2025-12-17
- Agent Task tool now renders progressively with live duration and completed sub-tools summary.
- Unified markdown rendering between assistant messages and tool outputs.
- Reduced markdown header sizes for better visual balance.
## [1.2.1] - 2025-12-16
- Todo task tracking: collapsible status row showing AI's current task and progress.
- Switched "Detailed" tool output mode to only open the 'task', 'edit', 'multiedit', 'write', 'bash' tools for better performance.
## [1.2.0] - 2025-12-15
- Favorite & recent models for quick access in model selection.
- Tool call expansion settings: collapsed, activity, or detailed modes.
- Font size & spacing controls (50-200% scaling) in Appearance Settings.
- Settings page access within VSCode extension.
Thanks to @theblazehen for contributing these features!
## [1.1.6] - 2025-12-15
- Optimized diff view layout with smaller fonts and compact hunk separators.
- Improved mobile experience: simplified header, better diff file selector.
- Redesigned password-protected session unlock screen.
## [1.1.5] - 2025-12-15
- Enhanced file attachment features performance.
- Added fuzzy search feature for file mentioning with @ in chat.
- Optimized input area layout.
## [1.1.4] - 2025-12-15
- Flexoki themes for Shiki syntax highlighting for consistency with the app color schema.
- Enchanced VSCode extension theming with editor themes.
- Fixed mobile view model/agent selection.
## [1.1.3] - 2025-12-14
- Replaced Monaco diff editor with Pierre/diffs for better performance.
- Added line wrap toggle in diff view with dynamic layout switching (auto-inline when narrow).
## [1.1.2] - 2025-12-13
- Moved VS Code extension to activity bar (left sidebar).
- Added feedback messages for "Restart API Connection" command.
- Removed redundant VS Code commands.
- Enhanced UserTextPart styling.
## [1.1.1] - 2025-12-13
- Adjusted model/agent selection alignment.
- Fixed user message rendering issues.
## [1.1.0] - 2025-12-13
- Added assistant answer fork flow so users can start a new session from an assistant plan/response with inherited context.
- Added OpenChamber VS Code extension with editor integration: file picker, click-to-open in tool parts.
- Improved scroll performance with force flag and RAF placeholder.
- Added git polling backoff optimization.
## [1.0.9] - 2025-12-08
- Added directory picker on first launch to reduce macOS permission prompts.
- Show changelog in update dialog from current to new version.
- Improved update dialog UI with inline version display.
- Added macOS folder access usage descriptions.
## [1.0.8] - 2025-12-08
- Added fallback detection for OpenCode CLI in ~/.opencode/bin.
- Added window focus after app restart/update.
- Adapted traffic lights position and corner radius for older macOS versions.
## [1.0.7] - 2025-12-08
- Optimized Opencode binary detection.
- Adjusted app update experience.
## [1.0.6] - 2025-12-08
- Enhance shell environment detection.
## [1.0.5] - 2025-12-07
- Fixed "Load older messages" incorrectly scrolling to bottom.
- Fixed page refresh getting stuck on splash screen.
- Disabled devtools and page refresh in production builds.
## [1.0.4] - 2025-12-07
- Optimized desktop app start time
## [1.0.3] - 2025-12-07
- Updated onboarding UI.
- Updated sidebar styles.
## [1.0.2] - 2025-12-07
- Updated MacOS window design to the latest one.
## [1.0.1] - 2025-12-07
- Initial public release of OpenChamber web and desktop packages in a unified monorepo.
- Added GitHub Actions release pipeline with macOS signing/notarization, npm publish, and release asset uploads.
- Introduced OpenCode agent chat experience with section-based navigation, theming, and session persistence.
-92
View File
@@ -1,92 +0,0 @@
# Contributing to OpenChamber
## Getting Started
```bash
git clone https://github.com/btriapitsyn/openchamber.git
cd openchamber
bun install
```
## Dev Scripts
### Web
| Script | Description | Ports |
|--------|-------------|-------|
| `bun run dev:web:full` | Build watcher + Express server. No HMR — manual refresh after changes. | `3001` (server + static) |
| `bun run dev:web:hmr` | Vite dev server + Express API. **Open the Vite URL for HMR**, not the backend. | `5180` (Vite HMR), `3902` (API) |
Both are configurable via env vars: `OPENCHAMBER_PORT`, `OPENCHAMBER_HMR_UI_PORT`, `OPENCHAMBER_HMR_API_PORT`.
### Desktop (Tauri)
```bash
bun run desktop:dev
```
Launches Tauri in dev mode with WebView devtools enabled and a distinct dev icon.
### VS Code Extension
```bash
bun run vscode:dev # Watch mode (extension + webview rebuild on save)
```
To test in VS Code:
```bash
bun run vscode:build && code --extensionDevelopmentPath="$(pwd)/packages/vscode"
```
### Shared UI (`packages/ui`)
No dev server — this is a source-level library consumed by other packages. During development, `bun run dev` runs type-checking in watch mode.
## Before Submitting
```bash
bun run type-check # Must pass
bun run lint # Must pass
bun run build # Must succeed
```
## Code Style
- Functional React components only
- TypeScript strict mode — no `any` without justification
- Use existing theme colors/typography from `packages/ui/src/lib/theme/` — don't add new ones
- Components must support light and dark themes
- Prefer early returns and `if/else`/`switch` over nested ternaries
- Tailwind v4 for styling; typography via `packages/ui/src/lib/typography.ts`
## Pull Requests
1. Fork and create a branch
2. Make changes
3. Run the validation commands above
4. Submit PR with clear description of what and why
## Project Structure
```
packages/
ui/ Shared React components, hooks, stores, and theme system
web/ Web server (Express) + frontend (Vite) + CLI
desktop/ Tauri macOS app (thin shell around the web UI)
vscode/ VS Code extension (extension host + webview)
```
See [AGENTS.md](./AGENTS.md) for detailed architecture reference.
## Not a developer?
You can still help:
- Report bugs or UX issues — even "this felt confusing" is valuable feedback
- Test on different devices, browsers, or OS versions
- Suggest features or improvements via issues
- Help others in Discord
## Questions?
Open an [issue](https://github.com/btriapitsyn/openchamber/issues) or ask in [Discord](https://discord.gg/ZYRSdnwwKA).
-22
View File
@@ -1,22 +0,0 @@
{
auto_https off
}
:3443 {
bind 0.0.0.0
tls /tmp/localhost.crt /tmp/localhost.key
reverse_proxy [::1]:3001 {
transport http {
read_timeout 120s
write_timeout 120s
compression off
}
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
header_down -Transfer-Encoding
flush_interval -1
}
}
-67
View File
@@ -1,67 +0,0 @@
# syntax=docker/dockerfile:1
FROM oven/bun:1 AS base
WORKDIR /app
FROM base AS deps
WORKDIR /app
COPY package.json bun.lock ./
COPY packages/ui/package.json ./packages/ui/
COPY packages/web/package.json ./packages/web/
COPY packages/desktop/package.json ./packages/desktop/
COPY packages/vscode/package.json ./packages/vscode/
RUN bun install --frozen-lockfile --ignore-scripts
FROM deps AS builder
WORKDIR /app
COPY . .
RUN bun run build:web
FROM oven/bun:1 AS runtime
WORKDIR /home/openchamber
RUN apt-get update && apt-get install -y --no-install-recommends \
bash \
ca-certificates \
git \
less \
nodejs \
npm \
openssh-client \
python3 \
&& rm -rf /var/lib/apt/lists/*
# Replace the base image's 'bun' user (UID 1000) with 'openchamber'
# so mounted volumes with 1000:1000 ownership work correctly.
RUN userdel bun \
&& groupadd -g 1000 openchamber \
&& useradd -u 1000 -g 1000 -m -s /bin/bash openchamber \
&& chown -R openchamber:openchamber /home/openchamber
# Switch to openchamber user
USER openchamber
ENV NPM_CONFIG_PREFIX=/home/openchamber/.npm-global
ENV PATH=${NPM_CONFIG_PREFIX}/bin:${PATH}
RUN npm config set prefix /home/openchamber/.npm-global && mkdir -p /home/openchamber/.npm-global && \
mkdir -p /home/openchamber/.local /home/openchamber/.config /home/openchamber/.ssh && \
npm install -g opencode-ai
# cloudflared 2026.3.0 - update digest explicitly when upgrading
COPY --from=cloudflare/cloudflared@sha256:6b599ca3e974349ead3286d178da61d291961182ec3fe9c505e1dd02c8ac31b0 /usr/local/bin/cloudflared /usr/local/bin/cloudflared
ENV NODE_ENV=production
COPY scripts/docker-entrypoint.sh /home/openchamber/openchamber-entrypoint.sh
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/packages/web/node_modules ./packages/web/node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/packages/web/package.json ./packages/web/package.json
COPY --from=builder /app/packages/web/bin ./packages/web/bin
COPY --from=builder /app/packages/web/server ./packages/web/server
COPY --from=builder /app/packages/web/dist ./packages/web/dist
EXPOSE 3000
ENTRYPOINT ["sh", "/home/openchamber/openchamber-entrypoint.sh"]
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Bohdan Triapitsyn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-403
View File
@@ -1,403 +0,0 @@
# <picture><source media="(prefers-color-scheme: dark)" srcset="docs/references/badges/openchamber-logo-dark.svg"><img src="docs/references/badges/openchamber-logo-light.svg" width="32" height="32" align="absmiddle" /></picture> OpenChamber
[![GitHub stars](https://img.shields.io/github/stars/btriapitsyn/openchamber?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iI2YxZWNlYyIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0yMjkuMDYsMTA4Ljc5bC00OC43LDQyLDE0Ljg4LDYyLjc5YTguNCw4LjQsMCwwLDEtMTIuNTIsOS4xN0wxMjgsMTg5LjA5LDczLjI4LDIyMi43NGE4LjQsOC40LDAsMCwxLTEyLjUyLTkuMTdsMTQuODgtNjIuNzktNDguNy00MkE4LjQ2LDguNDYsMCwwLDEsMzEuNzMsOTRMOTUuNjQsODguOGwyNC42Mi01OS42YTguMzYsOC4zNiwwLDAsMSwxNS40OCwwbDI0LjYyLDU5LjZMMjI0LjI3LDk0QTguNDYsOC40NiwwLDAsMSwyMjkuMDYsMTA4Ljc5WiIgb3BhY2l0eT0iMC4yIj48L3BhdGg%2BPHBhdGggZD0iTTIzOS4xOCw5Ny4yNkExNi4zOCwxNi4zOCwwLDAsMCwyMjQuOTIsODZsLTU5LTQuNzZMMTQzLjE0LDI2LjE1YTE2LjM2LDE2LjM2LDAsMCwwLTMwLjI3LDBMOTAuMTEsODEuMjMsMzEuMDgsODZhMTYuNDYsMTYuNDYsMCwwLDAtOS4zNywyOC44Nmw0NSwzOC44M0w1MywyMTEuNzVhMTYuMzgsMTYuMzgsMCwwLDAsMjQuNSwxNy44MkwxMjgsMTk4LjQ5bDUwLjUzLDMxLjA4QTE2LjQsMTYuNCwwLDAsMCwyMDMsMjExLjc1bC0xMy43Ni01OC4wNyw0NS0zOC44M0ExNi40MywxNi40MywwLDAsMCwyMzkuMTgsOTcuMjZabS0xNS4zNCw1LjQ3LTQ4LjcsNDJhOCw4LDAsMCwwLTIuNTYsNy45MWwxNC44OCw2Mi44YS4zNy4zNywwLDAsMS0uMTcuNDhjLS4xOC4xNC0uMjMuMTEtLjM4LDBsLTU0LjcyLTMzLjY1YTgsOCwwLDAsMC04LjM4LDBMNjkuMDksMjE1Ljk0Yy0uMTUuMDktLjE5LjEyLS4zOCwwYS4zNy4zNywwLDAsMS0uMTctLjQ4bDE0Ljg4LTYyLjhhOCw4LDAsMCwwLTIuNTYtNy45MWwtNDguNy00MmMtLjEyLS4xLS4yMy0uMTktLjEzLS41cy4xOC0uMjcuMzMtLjI5bDYzLjkyLTUuMTZBOCw4LDAsMCwwLDEwMyw5MS44NmwyNC42Mi01OS42MWMuMDgtLjE3LjExLS4yNS4zNS0uMjVzLjI3LjA4LjM1LjI1TDE1Myw5MS44NmE4LDgsMCwwLDAsNi43NSw0LjkybDYzLjkyLDUuMTZjLjE1LDAsLjI0LDAsLjMzLjI5UzIyNCwxMDIuNjMsMjIzLjg0LDEwMi43M1oiPjwvcGF0aD48L3N2Zz4%3D&logoColor=FFFCF0&labelColor=100F0F&color=66800B)](https://github.com/btriapitsyn/openchamber/stargazers)
[![GitHub release](https://img.shields.io/github/v/release/btriapitsyn/openchamber?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iI2YxZWNlYyIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0xMjgsMTI5LjA5VjIzMmE4LDgsMCwwLDEtMy44NC0xbC04OC00OC4xOGE4LDgsMCwwLDEtNC4xNi03VjgwLjE4YTgsOCwwLDAsMSwuNy0zLjI1WiIgb3BhY2l0eT0iMC4yIj48L3BhdGg%2BPHBhdGggZD0iTTIyMy42OCw2Ni4xNSwxMzUuNjgsMThhMTUuODgsMTUuODgsMCwwLDAtMTUuMzYsMGwtODgsNDguMTdhMTYsMTYsMCwwLDAtOC4zMiwxNHY5NS42NGExNiwxNiwwLDAsMCw4LjMyLDE0bDg4LDQ4LjE3YTE1Ljg4LDE1Ljg4LDAsMCwwLDE1LjM2LDBsODgtNDguMTdhMTYsMTYsMCwwLDAsOC4zMi0xNFY4MC4xOEExNiwxNiwwLDAsMCwyMjMuNjgsNjYuMTVaTTEyOCwzMmw4MC4zNCw0NC0yOS43NywxNi4zLTgwLjM1LTQ0Wk0xMjgsMTIwLDQ3LjY2LDc2bDMzLjktMTguNTYsODAuMzQsNDRaTTQwLDkwbDgwLDQzLjc4djg1Ljc5TDQwLDE3NS44MlptMTc2LDg1Ljc4aDBsLTgwLDQzLjc5VjEzMy44MmwzMi0xNy41MVYxNTJhOCw4LDAsMCwwLDE2LDBWMTA3LjU1TDIxNiw5MHY4NS43N1oiPjwvcGF0aD48L3N2Zz4%3D&logoColor=FFFCF0&labelColor=100F0F&color=205EA6)](https://github.com/btriapitsyn/openchamber/releases/latest)
[![Created with OpenCode](docs/references/badges/created-with-opencode.svg)](https://opencode.ai)
[![Discord](https://img.shields.io/badge/Discord-join.svg?style=flat&labelColor=100F0F&color=8B7EC8&logo=discord&logoColor=FFFCF0)](https://discord.gg/ZYRSdnwwKA)
[![Support the project](https://img.shields.io/badge/Support-Project-black?style=flat&labelColor=100F0F&color=EC8B49&logo=ko-fi&logoColor=FFFCF0)](https://ko-fi.com/G2G41SAWNS)
## **OpenCode, everywhere.** Desktop. Browser. Phone.
### A rich interface for [OpenCode](https://opencode.ai). Review diffs, manage agents, run dev servers, and keep the big picture while your AI codes.
![OpenChamber Chat](docs/references/chat_example.png)
<details>
<summary>More screenshots</summary>
![Tool Output](docs/references/tool_output_example.png)
![Settings](docs/references/settings_example.png)
![Web Version](docs/references/web_version_example.png)
![Diff View](docs/references/diff_example.png)
![VS Code Extension](packages/vscode/extension.jpg)
<p>
<img src="docs/references/pwa_chat_example.png" width="45%" alt="PWA Chat">
<img src="docs/references/pwa_diff_example.png" width="45%" alt="PWA Diff">
</p>
</details>
## Why use OpenChamber?
- **Cross-device continuity**: Start in TUI, continue on tablet/phone, return to terminal - same session
- **Remote access**: Use OpenCode from anywhere via browser
- **Familiarity**: A visual alternative for developers who prefer GUI workflows
## Features
### Core (all app versions)
- Branchable chat timeline with `/undo`, `/redo`, and one-click forks from earlier turns
- Smart tool UIs for diffs, file operations, permissions, and long-running task progress
- Voice mode with speech input and read-aloud responses for hands-free workflows
- Multi-agent runs from one prompt with isolated worktrees for safe side-by-side comparisons
- Git workflows in-app: identities, commits, PR creation, checks, and merge actions
- GitHub-native workflows: start sessions from issues and pull requests with context already attached
- Plan/Build mode with a dedicated plan view for drafting and iterating implementation steps
- Inline comment drafts on diffs, files, and plans that can be sent back to the agent
- Context visibility tools (token/cost breakdowns, raw message inspection, and activity summaries)
- Integrated terminal with per-directory sessions and stable performance on heavy output
- Built-in skills catalog and local skill management for reusable automation workflows
### Web / PWA
- Provider-aware tunnel access model with Cloudflare `quick`, `managed-remote`, and `managed-local` modes
- One-scan onboarding with tunnel QR + password URL helpers
- Mobile-first experience: optimized chat controls, keyboard-safe layouts, and attachment-friendly UI
- Background notifications plus reliable cross-tab session activity tracking
- Built-in self-update + restart flow that keeps your server settings intact
### Desktop (macOS)
- Native macOS menu integration with polished app actions and deep-link handling
- Multi-window support for parallel project/session workflows
- "Open In" shortcuts for Finder, Terminal, and your preferred editor
- Fast switching between local and remote instances
- Workspace-first startup flow with directory picker and steadier window restore behavior
### VS Code Extension
- Editor-native workflow: open files directly from tool output and keep sessions beside your code
- Agent Manager for parallel multi-model runs from a single prompt
- Right-click actions to add context, explain selections, and improve code in-place
- In-extension settings, responsive layout, and theme mapping that matches your editor
- Hardened runtime lifecycle and health checks for faster startup and fewer stuck reconnect states
### Custom Themes
- **Use it from anywhere** - Cloudflare tunnel with QR code onboarding. Scan, connect, code from your couch.
- **Branchable chat timeline** - Undo, redo, fork from any turn. Explore different approaches without losing your place.
- **GitHub-native workflows** - Start sessions from issues and PRs with context already attached. Review checks, merge - all in-app.
- **Project Actions** - Run dev servers, configure SSH port forwarding, open remote URLs locally. Your project commands, one click away.
- **Connect to remote machines** - Desktop app connects to remote OpenChamber instances over SSH, with dedicated lifecycle and UX flows.
## Quick Start
> **Prerequisite:** [OpenCode CLI](https://opencode.ai) installed.
### **Desktop (macOS)**
Download from [Releases](https://github.com/btriapitsyn/openchamber/releases).
### **VS Code**
Install from [Marketplace](https://marketplace.visualstudio.com/items?itemName=fedaykindev.openchamber) or search "OpenChamber" in Extensions.
### **CLI (Web + PWA)**
_requires Node.js 20+_
```bash
curl -fsSL https://raw.githubusercontent.com/btriapitsyn/openchamber/main/scripts/install.sh | bash
openchamber --ui-password be-creative-here
```
<details>
<summary>Advanced CLI options</summary>
```bash
openchamber --port 8080 # Custom port
openchamber --ui-password secret # Password-protect UI
openchamber tunnel help # Tunnel lifecycle commands
openchamber tunnel providers # Show provider capabilities
openchamber tunnel profile add --provider cloudflare --mode managed-remote --name prod-main --hostname app.example.com --token <token>
openchamber tunnel start --profile prod-main
openchamber tunnel start --provider cloudflare --mode quick --qr
openchamber tunnel start --provider cloudflare --mode managed-local --config ~/.cloudflared/config.yml
openchamber tunnel status --all # Show tunnel state across instances
openchamber tunnel stop --port 3000 # Stop tunnel only (server stays running)
openchamber logs # Follow latest instance logs
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external OpenCode server
OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber # Connect via custom host/HTTPS
openchamber stop # Stop server
openchamber update # Update to latest
```
Connect to an existing OpenCode server:
```bash
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber
OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber
```
Bind managed OpenCode server to all interfaces (use only on trusted networks):
```bash
OPENCHAMBER_OPENCODE_HOSTNAME=0.0.0.0 openchamber --port 3000
```
</details>
<details>
<summary>systemd service (VPN / LAN access)</summary>
Run OpenChamber and OpenCode as separate persistent services — useful when you want to access your
dev machine over a VPN (e.g. Tailscale) or LAN without a Cloudflare tunnel.
**How it works:**
- OpenCode runs as its own service, binding only to `localhost`.
- OpenChamber connects to it via `OPENCODE_HOST` and `--host 0.0.0.0` makes it reachable on your VPN IP.
- `--foreground` keeps the CLI process alive so systemd can track and restart it.
**`~/.config/systemd/user/opencode.service`**
```ini
[Unit]
Description=OpenCode Server
[Service]
Type=simple
ExecStart=opencode serve --port 4095
Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/YOU/.local/bin:/home/YOU/.npm-global/bin:/usr/local/bin:/usr/bin:/bin"
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
```
> **Why set `PATH` and `SSH_AUTH_SOCK`?**
> systemd user services start with a minimal environment — no shell profile is sourced.
> Without an explicit `PATH`, OpenCode won't find tools installed via Homebrew, npm, or `~/.local/bin`.
> Without `SSH_AUTH_SOCK`, git operations over SSH (push, pull, clone) will fail because the agent socket isn't inherited.
> Adjust the `PATH` to match your own tool installation paths.
> `%t` expands to `$XDG_RUNTIME_DIR` (e.g. `/run/user/1000`), where most SSH agents write their socket.
**`~/.config/systemd/user/openchamber.service`**
```ini
[Unit]
Description=OpenChamber Web Server
After=opencode.service
[Service]
Type=simple
ExecStart=openchamber serve --port 3000 --host 0.0.0.0 --ui-password your-password --foreground
Environment="OPENCODE_HOST=http://localhost:4095"
Environment="OPENCODE_SKIP_START=true"
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
```
```bash
systemctl --user daemon-reload
systemctl --user enable --now opencode openchamber
```
OpenChamber will be reachable at `http://<your-vpn-hostname>:3000` from any device on your VPN.
> **Note:** `--host 0.0.0.0` is required to listen on all interfaces. The default
> bind address is `127.0.0.1` (localhost only). Use `--host <ip>` or
> `OPENCHAMBER_HOST=<ip>` to bind to a specific interface instead.
</details>
<details>
<summary>Docker</summary>
```bash
docker compose up -d
```
Available at `http://localhost:3000`.
**UI Password:**
```yaml
environment:
UI_PASSWORD: your_secure_password
```
**Cloudflare Tunnel (optional):**
```yaml
environment:
OPENCHAMBER_TUNNEL_MODE: quick # quick | managed-remote | managed-local
OPENCHAMBER_TUNNEL_PROVIDER: cloudflare
```
For `managed-remote` mode, provide:
```yaml
environment:
OPENCHAMBER_TUNNEL_MODE: managed-remote
OPENCHAMBER_TUNNEL_HOSTNAME: app.example.com
OPENCHAMBER_TUNNEL_TOKEN: <token>
```
For `managed-local` mode, optionally provide:
```yaml
environment:
OPENCHAMBER_TUNNEL_MODE: managed-local
OPENCHAMBER_TUNNEL_CONFIG: /home/openchamber/.cloudflared/config.yml
```
Managed-local path note: `OPENCHAMBER_TUNNEL_CONFIG` must point to a path inside the container user home (`/home/openchamber/...`). If your Cloudflare config references a credentials JSON file, that file path must also be accessible inside the container (mount with `volumes`).
### Reverse proxy notes
- For a complete reverse proxy setup guide, see [`docs/REVERSE_PROXY.md`](./docs/REVERSE_PROXY.md).
- Website docs source lives at `packages/docs/content/docs/reverse-proxy.mdx`.
### Tunnel behavior notes
- OpenChamber supports one active tunnel per running instance (port).
- Starting a tunnel with a different mode/provider on the same instance replaces the current tunnel.
- Replacing or stopping a tunnel revokes existing connect links and invalidates remote tunnel sessions for that instance.
- Connect links are one-time tokens; generating a new link revokes the previous unused link.
**Data Directory Permission Note:** The `data/` directory is mounted into the container for persistent storage (config, sessions, SSH keys, workspaces). Before running, ensure the directory exists and has proper permissions:
```bash
mkdir -p data/openchamber data/opencode/share data/opencode/config data/ssh
chown -R 1000:1000 data/
```
**SSH/Git:** If git push/pull fails, run `ssh -T git@github.com` in terminal.
</details>
## Features
<details>
<summary><strong>Chat & Interaction</strong></summary>
- Branchable chat timeline with `/undo`, `/redo`, and one-click forks from any turn
- Multi-agent runs from one prompt with isolated worktrees for safe side-by-side comparisons
- Voice mode with speech input and read-aloud responses for hands-free workflows
- Plan/Build mode with a dedicated plan view for drafting and iterating steps
- Inline comment drafts on diffs, files, and plans - send feedback back to the agent
- Shell mode via leading `!` with inline output
- Share messages as images
- Mermaid diagrams render inline with copy/download actions
- Smart tool UIs for diffs, file operations, permissions, and task progress
</details>
<details>
<summary><strong>Git & GitHub</strong></summary>
- Full Git sidebar with staging, commits, push/pull, branch management, and rebase/merge flows
- PR creation with AI-generated descriptions, status checks, and merge actions
- Start sessions from GitHub issues and pull requests with context baked in
- Multi-remote push and fork-aware PR creation
- Worktree integration: isolated sessions per branch, merge back with conflict handling
- Git identities, gitmoji support, and multi-account GitHub auth
</details>
<details>
<summary><strong>Files, Diff & Terminal</strong></summary>
- Workspace file browser with inline editing, syntax highlighting, and markdown preview
- Beautiful diff viewer with stacked/inline modes, lazy loading for large changesets
- Integrated terminal with per-directory sessions, tabbed interface, and stable heavy-output performance
- Clickable file paths in messages - jump to exact line locations
- File-type icons across all views for faster visual scanning
</details>
<details>
<summary><strong>Web / PWA</strong></summary>
- Cloudflare tunnel with quick, managed-remote, and managed-local modes, secure one-time connect links, and QR onboarding
- Mobile-first: optimized chat controls, keyboard-safe layouts, drag-to-reorder projects
- Background notifications and cross-tab session tracking
- Self-update + restart flow that keeps your server settings intact
- Installable as PWA with project-aware naming
</details>
<details>
<summary><strong>Desktop (macOS)</strong></summary>
- Connect to remote OpenChamber instances over SSH with dedicated lifecycle flows
- Project Actions: run dev servers, SSH port forwarding, open remote URLs locally
- Multi-window support for parallel project workflows
- "Open In" shortcuts for Finder, Terminal, and your preferred editor
- Fast switching between local and remote instances
- Native macOS menu, deep-link handling, and polished startup
</details>
<details>
<summary><strong>VS Code Extension</strong></summary>
- Editor-native: open files from tool output, keep sessions beside your code
- Agent Manager for parallel multi-model runs from a single prompt
- Right-click actions: add context, explain selections, improve code in-place
- Session editor panel, responsive layout, and theme mapping to your editor
- Edit-style tool results open directly in focused diff views
</details>
<details>
<summary><strong>Customization</strong></summary>
- 18+ built-in themes with light/dark variants
- Custom themes via JSON files in `~/.config/openchamber/themes/` - hot reload, no restart
- Configurable keyboard shortcuts for chat, panels, and services
- Font size, spacing, corner radius, and layout controls
- Customizable project icons with upload and automatic favicon discovery
- Skills catalog and local skill management for reusable automation
[Read the Guide: Custom Themes](docs/CUSTOM_THEMES.md)
</details>
<details>
<summary><strong>Context & Productivity</strong></summary>
- Token usage, cost breakdowns, and raw message inspection panel
- Usage quota tracking across multiple providers with pace/prediction indicators
- Favorite model cycling via keyboard shortcuts
- Session folders and subfolders with drag-to-reorder
- Persistent project notes and todos per project
- Draft persistence per session with expanded focus mode for longer prompts
</details>
## Roadmap
Active development. Here's what's being worked on or planned:
- Windows and Linux desktop apps
- Mobile app with remote instance and laptop connectivity
- More built-in tunneling options
- Kanban board for multi-agent management - keeping the human in the loop and in control
- Custom OpenCode plugins/tools built-in catalog
- Linear integration
- Built-in browser for running dev apps with agent integration
## Acknowledgments
Independent project, not affiliated with the OpenCode team.
**Special thanks to:**
- [OpenCode](https://opencode.ai) - For the excellent API and extensible architecture.
- [Flexoki](https://github.com/kepano/flexoki) - Beautiful color scheme by [Steph Ango](https://stephango.com/flexoki).
- [Pierre](https://pierrejs-docs.vercel.app/) - Fast, beautiful diff viewer with syntax highlighting.
- [Tauri](https://github.com/tauri-apps/tauri) - Desktop application framework.
- [Ghostty-web](https://github.com/coder/ghostty-web) - Great implementation of a Ghostty web renderer.
- [David Hill](https://x.com/iamdavidhill) - Who inspired me to release this without [overthinking](https://x.com/iamdavidhill/status/1993648326450020746).
- [My wife](https://github.com/yulia-ivashko), who - with zero AI background - sat down with the app for the first time and built the firework celebration that plays on every successful push.
- Every contributor who shaped this project with their PRs, ideas, and attention to detail.
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and guidelines.
Docs source lives in [`packages/docs`](packages/docs/README.md).
## License
MIT
-32
View File
@@ -1,32 +0,0 @@
# Security Policy
## Reporting a Vulnerability
If you discover a security vulnerability in OpenChamber, please report it responsibly.
**Email:** [artmore@protonmail.com](mailto:artmore@protonmail.com)
Please include:
- Description of the vulnerability
- Steps to reproduce
- Affected version(s)
- Potential impact
I'll acknowledge receipt within 48 hours and aim to provide a fix or mitigation as quickly as possible.
**Please do not open public GitHub issues for security vulnerabilities.**
## Scope
OpenChamber handles sensitive context including:
- UI authentication (password-protected sessions, JWT tokens)
- Cloudflare tunnel access (remote connectivity)
- Terminal access (PTY sessions)
- Git credentials and SSH keys
- File system operations
Security reports related to any of these areas are especially appreciated.
## Supported Versions
Security fixes are applied to the latest release. There is no LTS or backport policy at this time.
-3876
View File
File diff suppressed because it is too large Load Diff
-21
View File
@@ -1,21 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
-30
View File
@@ -1,30 +0,0 @@
services:
openchamber:
build:
context: .
dockerfile: Dockerfile
container_name: openchamber
ports:
- "3000:3000"
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./data/openchamber:/home/openchamber/.config/openchamber
- ./data/opencode/share:/home/openchamber/.local/share/opencode
- ./data/opencode/state:/home/openchamber/.local/state/opencode
- ./data/opencode/config:/home/openchamber/.config/opencode
- ./data/ssh:/home/openchamber/.ssh
- ./workspaces:/home/openchamber/workspaces
#environment:
# OPENCHAMBER_HOST: 0.0.0.0 # Bind address (default in Docker: 0.0.0.0)
# UI_PASSWORD: your_secure_password_here # Uncomment to set UI password
# OPENCHAMBER_TUNNEL_PROVIDER: cloudflare
# OPENCHAMBER_TUNNEL_MODE: quick # quick | managed-remote | managed-local
# OPENCHAMBER_TUNNEL_HOSTNAME: app.example.com # required for managed-remote
# OPENCHAMBER_TUNNEL_TOKEN: your_cloudflare_token # required for managed-remote
# OPENCHAMBER_TUNNEL_CONFIG: /home/openchamber/.cloudflared/config.yml # optional for managed-local
# OH_MY_OPENCODE: true # enable oh-my-opencode
# OPENCODE_HOST: http://172.17.0.1:4096 # Connect to external OpenCode server
# OPENCODE_SKIP_START: true # skip start opencode
# OPENCHAMBER_OPENCODE_HOSTNAME: 0.0.0.0 # Bind OpenCode to all interfaces
restart: unless-stopped
View File
-229
View File
@@ -1,229 +0,0 @@
# Custom Themes
OpenChamber supports user-defined themes. Drop a JSON file into the themes directory and reload — no app restart required.
## Quick Start
1. Create the themes directory:
```bash
mkdir -p ~/.config/openchamber/themes
```
2. Create a theme JSON file (e.g., `my-theme.json`) with the format below.
3. In OpenChamber: **Settings → Theme → Reload themes**.
4. Select your theme from the dropdown.
## Theme Location
| Platform | Path |
|----------|------|
| macOS/Linux | `~/.config/openchamber/themes/` |
## Theme Format
```json
{
"metadata": {
"id": "my-custom-theme",
"name": "My Custom Theme",
"description": "A custom theme for OpenChamber",
"version": "1.0.0",
"variant": "dark",
"tags": ["dark", "custom"]
},
"colors": {
"primary": {
"base": "#EC8B49",
"hover": "#DA702C",
"active": "#F9AE77",
"foreground": "#100F0F",
"muted": "#EC8B4980",
"emphasis": "#F9AE77"
},
"surface": {
"background": "#100F0F",
"foreground": "#CECDC3",
"muted": "#1C1B1A90",
"mutedForeground": "#878580",
"elevated": "#1C1A1990",
"elevatedForeground": "#CECDC3",
"overlay": "#00000080",
"subtle": "#1e1d1c"
},
"interactive": {
"border": "#343331",
"borderHover": "#403E3C",
"borderFocus": "#EC8B49",
"selection": "#f4f4f41f",
"selectionForeground": "#CECDC3",
"focus": "#EC8B49",
"focusRing": "#EC8B4950",
"cursor": "#CECDC3",
"hover": "#ffffff18",
"active": "#ffffff1f"
},
"status": {
"error": "#D14D41",
"errorForeground": "#100F0F",
"errorBackground": "#AF302920",
"errorBorder": "#AF302950",
"warning": "#DA702C",
"warningForeground": "#100F0F",
"warningBackground": "#BC521520",
"warningBorder": "#BC521550",
"success": "#A0AF54",
"successForeground": "#100F0F",
"successBackground": "#66800B20",
"successBorder": "#66800B50",
"info": "#4385BE",
"infoForeground": "#100F0F",
"infoBackground": "#205EA620",
"infoBorder": "#205EA650"
},
"pr": {
"open": "#A0AF54",
"draft": "#878580",
"blocked": "#DA702C",
"merged": "#8B7EC8",
"closed": "#D14D41"
},
"syntax": {
"base": {
"background": "#1C1B1A",
"foreground": "#CECDC3",
"comment": "#878580",
"keyword": "#4385BE",
"string": "#3AA99F",
"number": "#8B7EC8",
"function": "#DA702C",
"variable": "#CECDC3",
"type": "#D0A215",
"operator": "#D14D41"
},
"tokens": {
"commentDoc": "#575653",
"stringEscape": "#CECDC3",
"keywordImport": "#D14D41",
"storageModifier": "#4385BE",
"functionCall": "#DA702C",
"method": "#879A39",
"variableProperty": "#4385BE",
"variableOther": "#879A39",
"variableGlobal": "#CE5D97",
"variableLocal": "#282726",
"parameter": "#CECDC3",
"constant": "#CECDC3",
"class": "#DA702C",
"className": "#DA702C",
"interface": "#D0A215",
"struct": "#DA702C",
"enum": "#DA702C",
"typeParameter": "#DA702C",
"namespace": "#D0A215",
"module": "#D14D41",
"tag": "#4385BE",
"jsxTag": "#CE5D97",
"tagAttribute": "#D0A215",
"tagAttributeValue": "#3AA99F",
"boolean": "#D0A215",
"decorator": "#D0A215",
"label": "#CE5D97",
"punctuation": "#878580",
"macro": "#4385BE",
"preprocessor": "#CE5D97",
"regex": "#3AA99F",
"url": "#4385BE",
"key": "#DA702C",
"exception": "#CE5D97"
},
"highlights": {
"diffAdded": "#879A39",
"diffAddedBackground": "#66800B20",
"diffRemoved": "#D14D41",
"diffRemovedBackground": "#AF302920",
"diffModified": "#4385BE",
"diffModifiedBackground": "#205EA620",
"lineNumber": "#403E3C",
"lineNumberActive": "#CECDC3"
}
},
"markdown": {
"heading1": "#fbf9e6",
"heading2": "#e6e4d2",
"heading3": "#CECDC3",
"heading4": "#CECDC3",
"link": "#4385BE",
"linkHover": "#205EA6",
"inlineCode": "#A0AF53",
"inlineCodeBackground": "#1C1B1A",
"blockquote": "#878580",
"blockquoteBorder": "#343331",
"listMarker": "#D0A21599"
},
"chat": {
"userMessage": "#CECDC3",
"userMessageBackground": "#2d1d15",
"assistantMessage": "#CECDC3",
"assistantMessageBackground": "#100F0F",
"timestamp": "#878580",
"divider": "#343331"
},
"tools": {
"background": "#1C1B1A50",
"border": "#42403e9d",
"headerHover": "#34333150",
"icon": "#aca7a1",
"title": "#CECDC3",
"description": "#878580",
"edit": {
"added": "#879A39",
"addedBackground": "#66800B25",
"removed": "#D14D41",
"removedBackground": "#AF302925",
"lineNumber": "#403E3C"
}
}
},
"config": {
"fonts": {
"sans": "\"IBM Plex Mono\", monospace",
"mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace"
},
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": {
"fast": "150ms ease",
"normal": "250ms ease",
"slow": "350ms ease"
}
}
}
```
## Surface Alpha Requirement
- `colors.surface.muted` and `colors.surface.elevated` must always use 90 alpha (`...90` in 8-digit hex, e.g. `#1C1B1A90`).
## Validation
Themes are validated on load. Invalid themes are skipped with a console warning.
Common issues:
- Missing required fields
- Invalid `variant` (must be `"light"` or `"dark"`)
- File size > 512KB
## Tips
- Use hex with alpha for transparency (e.g., `#FFFFFF20`)
- Reference built-in themes in `packages/ui/src/lib/theme/themes/` for more examples
- Theme `id` must be unique; duplicates are skipped
-337
View File
@@ -1,337 +0,0 @@
# Reverse Proxy Setup
Use this guide when running OpenChamber behind Nginx, Nginx Proxy Manager, Caddy, Cloudflare, or another reverse proxy.
## Before you proxy it
1. Confirm OpenChamber works directly first.
2. Open `http://<server-ip>:3000` or your custom port from the same network.
3. Only add the reverse proxy after the direct connection works.
## What the proxy must support
- WebSockets for live message transport:
- `/api/event/ws`
- `/api/global/event/ws`
- `/api/terminal/ws`
- SSE without buffering:
- `/api/event`
- `/api/global/event`
- `/api/notifications/stream`
- `/api/openchamber/events`
- `/api/terminal/:sessionId/stream`
- Large request bodies for attachments and file operations
- Long-lived read timeouts for live streams and terminal sessions
## Rules that matter
- Enable WebSocket proxying.
- Disable buffering on SSE routes.
- Disable gzip on the proxy if OpenChamber is already compressing responses.
- Keep compression enabled in only one layer.
- Forward normal proxy headers such as `Host`, `X-Forwarded-For`, and `X-Forwarded-Proto`.
- Increase body size limits if users upload files.
## Quick checklist
- OpenChamber reachable directly on LAN
- WebSockets enabled in the proxy
- SSE routes have buffering off
- `gzip off` on the proxy host, or proxy compression disabled another way
- `client_max_body_size` large enough for attachments
- `proxy_read_timeout` long enough for streams
## Example: Nginx
<details>
<summary>Show example config</summary>
```nginx
client_max_body_size 50M;
client_body_buffer_size 50M;
proxy_request_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
gzip off;
location = /api/terminal/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location = /api/global/event/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location = /api/event/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location ~ ^/api/(event|global/event|notifications/stream|openchamber/events)$ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location ~ ^/api/terminal/.+/stream$ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location /api {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
```
</details>
## Example: Nginx Proxy Manager
<details>
<summary>Show Advanced tab example</summary>
```nginx
client_max_body_size 50M;
client_body_buffer_size 50M;
proxy_request_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
gzip off;
location = /api/terminal/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/global/event/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/event/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/event {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/global/event {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/notifications/stream {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/openchamber/events {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location ~ ^/api/terminal/.+/stream$ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location /api {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
```
</details>
Also enable `Websockets Support` in Nginx Proxy Manager for this host.
## Common failure signs
### Page loads, but sending messages fails
- WebSockets are not enabled in the proxy
- `/api/event/ws` or `/api/global/event/ws` is not passing through correctly
### Notifications or live status do not update
- one of the SSE routes is buffered or cached
- `X-Accel-Buffering "no"` is missing
### File uploads fail
- `client_max_body_size` is too small
### Everything works locally, but breaks only behind the proxy
- the proxy is compressing and buffering live traffic
- the proxy is missing WebSocket support
## Example: Caddy
<details>
<summary>Show example config</summary>
```caddy
reverse_proxy 127.0.0.1:3000 {
# WebSocket support is automatic in Caddy
# Flush SSE responses immediately
flush_interval -1
# Pass through Host and proxy headers
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
# Increase timeouts for long-lived streams
transport http {
read_timeout 3600s
write_timeout 3600s
}
}
```
</details>
Caddy handles WebSocket upgrades automatically — no extra configuration needed. The `flush_interval -1` directive ensures SSE chunks are forwarded immediately without buffering.
## CDN and double-compression warning
If you place a CDN (such as Cloudflare) in front of your reverse proxy, be aware of double compression:
- OpenChamber compresses HTTP responses with gzip (threshold 1 KB).
- Cloudflare and other CDNs also compress responses by default.
- This can cause double-compressed responses or incorrect `Content-Encoding` headers.
To avoid this, disable compression at **one** layer:
- **Cloudflare:** Rules → Compression → disable (or use "Passthrough" mode).
- **Nginx:** `gzip off` (already shown in the examples above).
- **Caddy:** Caddy does not re-compress by default if the upstream already sends compressed content.
SSE streaming routes are excluded from compression by OpenChamber, but the CDN may still buffer them. Check your CDN documentation for how to disable buffering on SSE paths.
-349
View File
@@ -1,349 +0,0 @@
# Tauri → Electron auto-update cutover
> Self-contained playbook. The branch and conversation where this plan was
> designed will not be around when the cutover happens — read this file top to
> bottom and execute; do not assume prior context.
## What this is
OpenChamber historically shipped as a Tauri app. A parallel Electron shell was
added on branch `electron-app` (merged to `main` as part of a larger migration).
Since then, both desktop shells have been released in the same GitHub release
and each has its own auto-update channel:
| Shell | Manifest | Update format | Secret used to sign |
|----------|-------------------|---------------------|---------------------|
| Tauri | `latest.json` | `.tar.gz` + `.sig` | `TAURI_SIGNING_PRIVATE_KEY` (minisign) |
| Electron | `latest-mac.yml` | `.zip` + `blockmap` | Developer ID codesign (APPLE_* secrets) |
Existing Tauri installs keep their own auto-update path (`latest.json`).
Electron installs auto-update through `latest-mac.yml`. They coexist without
conflict because filenames and manifests differ.
At some point the user wants to **stop maintaining the Tauri build** and make
the Tauri installs migrate themselves into Electron via auto-update. This
document describes how to do that in a single "transition release".
## The core trick
Tauri's updater downloads whatever `.tar.gz` the `latest.json` points at,
verifies the minisign signature, unpacks the contents **over** the existing
`.app` directory, and restarts. It does **not** introspect the payload — it
just replaces files.
So: produce a `.tar.gz` of the Electron `.app`, sign it with the existing
Tauri minisign key, point `latest.json` at it. Tauri users receive the update,
their `OpenChamber.app` becomes the Electron bundle in-place, and next launch
starts Electron. Subsequent updates go through `latest-mac.yml`
(electron-updater). One-way migration, one-shot workflow change.
## Prerequisites before running the cutover
Check all of these before making any release:
1. **Electron has shipped stable through its own `latest-mac.yml` path for at
least 2 releases.** Verify:
```
gh release list --repo btriapitsyn/openchamber
gh release view vX.Y.Z --repo btriapitsyn/openchamber \
| grep -E 'OpenChamber-.*\.zip|latest-mac\.yml'
```
A user on Electron should have successfully auto-updated at least once.
If not, pause and stabilise that path first — don't stack risk.
2. **`~/.config/openchamber/settings.json` is still the shared state path.**
Tauri `src-tauri/src/main.rs:settings_file_path` and Electron
`packages/electron/main.mjs:settingsFilePath` must both resolve to
`$HOME/.config/openchamber/settings.json`. If either has moved, data parity
breaks and this migration loses user data. Audit both paths, update the
non-migrated shell to match before proceeding.
3. **Electron `appId` is `dev.openchamber.desktop`** (check
`packages/electron/package.json` `build.appId`). Tauri identifier is
`ai.opencode.openchamber`. These differ intentionally — it means macOS
LaunchServices will re-register after the in-place replace. That's fine but
see "Risks" below.
4. **All GitHub secrets still valid:** `APPLE_CERTIFICATE`,
`APPLE_CERTIFICATE_PASSWORD`, `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID`,
`TAURI_SIGNING_PRIVATE_KEY`, `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`. A
workflow_dispatch dry-run should succeed before the real tag.
5. **`minisign` CLI is available on the macOS runner** (or installable via
brew). Used to sign the Electron tarball with the Tauri key.
## Release workflow changes
The file to edit: `.github/workflows/release.yml`.
Today it has these jobs (simplified):
```
create-release
├── build-desktop-macos (Tauri .dmg/.tar.gz/.tar.gz.sig)
├── build-desktop-electron-macos (Electron .dmg/.zip/blockmap/latest-mac.yml)
├── publish-npm
├── combine-manifests (merges Tauri per-arch JSONs → latest.json)
├── combine-electron-manifests (merges Electron per-arch YMLs → latest-mac.yml)
└── finalize-release
```
### Step 1 — Remove the Tauri build
Delete these jobs entirely:
- `build-desktop-macos`
- `combine-manifests`
They are replaced by the repackage job (below). `finalize-release` `needs:`
list must be updated to drop both.
### Step 2 — Add a repackage job
Insert after `build-desktop-electron-macos`:
```yaml
repackage-electron-as-tauri-update:
needs: [create-release, build-desktop-electron-macos]
runs-on: macos-26
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
platform: darwin-aarch64
- arch: x64
platform: darwin-x86_64
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: '20'
# Pull the signed+notarized Electron .app that build-desktop-electron-macos
# already produced. Either re-download the dmg and mount+copy the .app, or
# (cleaner) modify build-desktop-electron-macos to upload the .app itself
# as an artifact so this job can download it. Prefer the latter — adds one
# `actions/upload-artifact@v4` step uploading `packages/electron/dist/mac-<arch>/OpenChamber.app`.
- name: Download signed Electron .app
uses: actions/download-artifact@v4
with:
name: electron-app-${{ matrix.arch }}
path: staged
- name: Install minisign
run: brew install minisign
- name: Tar and sign Electron .app as Tauri update payload
env:
TAURI_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VERSION: ${{ needs.create-release.outputs.version }}
run: |
set -euo pipefail
cd staged
# The tarball name convention Tauri's updater expects. Must end in
# `.app.tar.gz`. Name stays stable — Tauri updater does not care about
# the inner .app name.
TARBALL="OpenChamber.app.tar.gz"
tar -czf "$TARBALL" OpenChamber.app
# minisign needs the private key written to a file and a non-interactive
# password via -W (or env). The key in the secret is a minisign secret
# key block (base64-ish multi-line blob). Write to a file verbatim.
echo "$TAURI_KEY" > ../tauri-signing.key
echo "$TAURI_KEY_PASSWORD" | minisign -S -s ../tauri-signing.key \
-m "$TARBALL" -W
# Rename per platform so the release has distinct names for arm64/x64.
mv "$TARBALL" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz"
mv "${TARBALL}.minisig" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz.sig"
- name: Generate Tauri latest-<platform>.json
env:
VERSION: ${{ needs.create-release.outputs.version }}
REPO: ${{ github.repository }}
run: |
SIG=$(cat staged/OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz.sig)
TAR=OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz
cat > staged/latest-${{ matrix.platform }}.json <<EOF
{
"version": "${VERSION}",
"notes": "OpenChamber has moved to Electron. This update replaces the Tauri shell with the Electron build. Subsequent updates will be delivered via the Electron auto-updater.",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"${{ matrix.platform }}": {
"signature": "${SIG}",
"url": "https://github.com/${REPO}/releases/download/v${VERSION}/${TAR}"
}
}
}
EOF
- name: Upload tarball + sig to release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.create-release.outputs.version }}
files: |
staged/*.app.tar.gz
staged/*.app.tar.gz.sig
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload per-platform manifest as artifact for merge
uses: actions/upload-artifact@v4
with:
name: tauri-manifest-${{ matrix.platform }}
path: staged/latest-${{ matrix.platform }}.json
retention-days: 1
```
### Step 3 — Re-add the `combine-manifests` job
Bring it back (it was deleted in Step 1) but sourcing artifacts from the
repackage job instead of the old Tauri build. The merging logic is identical
to what the old job did. Minimum job shape:
```yaml
combine-manifests:
needs: [create-release, repackage-electron-as-tauri-update]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: tauri-manifest-*
path: artifacts
- name: Merge
run: |
# Copy the original merge logic from git history. It takes the two
# per-platform JSONs and produces a single `latest.json` with both
# platform entries. Upload as a release asset.
# Search git history: git log --all --diff-filter=D -- .github/workflows/release.yml
# Find the commit that deleted the old merge step and copy its shell block.
...
- uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.create-release.outputs.version }}
files: artifacts/latest.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
### Step 4 — Update `finalize-release.needs`
```yaml
finalize-release:
needs: [create-release, build-desktop-electron-macos, repackage-electron-as-tauri-update, publish-npm, combine-manifests, combine-electron-manifests]
```
### Step 5 — Remove Tauri-specific code
After the transition release ships and has been out at least 2 weeks with no
rollback, remove:
- `packages/desktop/` (entire package — Tauri Rust + UI glue)
- Any `isTauriShell()` branches that are now dead code in
`packages/ui/src/` (search for the symbol; most call sites already fall
through to the Electron path because our preload exposes a `__TAURI__` shim;
audit each before removing).
- This file (`docs/TAURI_TO_ELECTRON_CUTOVER.md`) — mission accomplished.
Do this in a separate PR. Keep the transition release workflow intact until
the cleanup lands; rolling the cleanup into the transition release itself
makes debugging much harder if the migration misbehaves for a user.
## Validation before tagging the transition release
You must manually validate with a real Tauri install. Do NOT skip this.
1. Have the previous Tauri release installed locally
(`/Applications/OpenChamber.app` with `Contents/Info.plist` showing
`CFBundleIdentifier = ai.opencode.openchamber`).
2. Tag the transition release to a test tag
(e.g. `v2.0.0-migration-test`) and push.
3. Let the workflow complete. Do not merge cleanup PR yet.
4. In the running Tauri app, use the built-in "Check for updates".
5. Accept the update. The app should download, verify, extract, restart.
6. After restart, `Info.plist` under `/Applications/OpenChamber.app/` should
now show `CFBundleIdentifier = dev.openchamber.desktop`.
7. Settings should be intact: hosts list, default host, sessions history.
8. In the new Electron app, "Check for updates" should report no update
available (it's now at the transition version, which is the latest).
9. Produce a dummy v2.0.1 Electron-only release to prove the subsequent
Electron-path update works. Accept it. App relaunches into v2.0.1.
If any step fails:
- Delete the test tag and GitHub release.
- Do not delete yet-shipped artifacts from a real tag until rollback below.
## Rollback if the transition release misbehaves
If users report the Tauri → Electron update bricks their install:
1. **Immediately** delete the latest release asset
`OpenChamber-*.app.tar.gz` and `latest.json` from the GitHub release
(keep the DMGs so manual download still works).
2. Re-upload the previous version's `latest.json` as the current latest so
Tauri updaters see "up to date" instead of a broken update on next check.
3. Post a support note: users who already applied the broken update can
download a fresh Electron `.dmg` manually and drag-replace. Their
`~/.config/openchamber/settings.json` survives.
4. Investigate, fix the workflow, retry with a new version number.
## Risks & edge cases
### Different `CFBundleIdentifier` at same path
macOS LaunchServices caches identifier ↔ path mappings. When we replace
`ai.opencode.openchamber` with `dev.openchamber.desktop` at the same `.app`
path, LaunchServices will rebuild on next launch (automatic). Usually fine.
If a user's system is in a weird state, a `killall Dock` or logout/login
fixes it. Worth noting in the release notes.
### macOS notification permissions
Notification permission is per-bundle-id. After migration, the app has a new
bundle-id, so the first notification will re-prompt the user. Unavoidable.
Mention in release notes.
### Deep-link protocol registration
The `openchamber://` protocol was registered for `ai.opencode.openchamber`.
After migration, `dev.openchamber.desktop` registers itself on first launch.
LaunchServices updates the handler. Usually seamless. Test with
`open 'openchamber://session/test'` post-migration.
### Gatekeeper "damaged app" dialog
Rare. Triggered if the replaced `.app` fails a mid-extract codesign check.
Can happen if Tauri's extractor corrupts xattrs. Mitigation: test on a
pristine macOS install before tagging production.
### Users on unsupported old Tauri versions
If a user is on a very old Tauri build that doesn't know how to do the
fetch-verify-extract flow, they're stuck. Expected: negligibly few users;
they'll just stay on their old version forever until they manually download.
Acceptable.
### Rollback-after-migration-accepted is impossible per-user
Once a user is on Electron, the Tauri updater is gone. If they want to go
back to a Tauri build, they must manually download. We don't support this.
## Relevant files to understand before making changes
- `.github/workflows/release.yml` — the release workflow.
- `packages/electron/package.json` — electron-builder config (appId,
mac/dmg, publish, artifactName).
- `packages/electron/main.mjs` — autoUpdater setup (`setupAutoUpdater`,
`desktop_check_for_updates`, `desktop_download_and_install_update`,
`desktop_restart`). Understand this flow before touching the CI.
- `packages/electron/scripts/finalize-latest-yml.mjs` — per-arch
`latest-mac.yml` merger. Already wired in `combine-electron-manifests`.
- `packages/desktop/src-tauri/tauri.conf.json` — legacy Tauri identifier,
minisign pubkey embedded for updater verification. Don't modify; just
reference for context.
## Working protocol
Default to a dry-run (test tag like `vX.Y.Z-migration-test` on a workflow_dispatch
run) before the real tag. Surface only business-level decisions —
"cutover this release, or hold one more cycle?" — and make technical calls
(minisign invocation flags, YAML layout, job dependency order) yourself,
documenting each one in the PR description.
@@ -1,19 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="98" height="20" role="img" aria-label="Created with OpenCode">
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="r">
<rect width="98" height="20" rx="3" fill="#fff"/>
</clipPath>
<g clip-path="url(#r)">
<rect width="86" height="20" fill="#100F0F"/>
<rect x="86" width="12" height="20" fill="#100F0F"/>
<rect width="98" height="20" fill="url(#s)"/>
</g>
<text x="4.5" y="14" fill="#FFFCF0" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11" text-anchor="start">Created with</text>
<g transform="translate(82 4) scale(0.3)">
<path d="M24 32H8V16H24V32Z" fill="#4B4646"/>
<path d="M24 8H8V32H24V8ZM32 40H0V0H32V40Z" fill="#F1ECEC"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

@@ -1,47 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Left face -->
<path d="M50 50 L8.432 26 L8.432 74 L50 98 Z" fill="white" fill-opacity="0.15" stroke="white" stroke-width="2" stroke-linejoin="round"/>
<!-- Left face grid cells -->
<path d="M50 50 L39.608 44 L39.608 56 L50 62 Z" fill="white" fill-opacity="0.07"/>
<path d="M39.608 44 L29.216 38 L29.216 50 L39.608 56 Z" fill="white" fill-opacity="0.16"/>
<path d="M29.216 38 L18.824 32 L18.824 44 L29.216 50 Z" fill="white" fill-opacity="0.05"/>
<path d="M18.824 32 L8.432 26 L8.432 38 L18.824 44 Z" fill="white" fill-opacity="0.19"/>
<path d="M50 62 L39.608 56 L39.608 68 L50 74 Z" fill="white" fill-opacity="0.12"/>
<path d="M39.608 56 L29.216 50 L29.216 62 L39.608 68 Z" fill="white" fill-opacity="0.04"/>
<path d="M29.216 50 L18.824 44 L18.824 56 L29.216 62 Z" fill="white" fill-opacity="0.18"/>
<path d="M18.824 44 L8.432 38 L8.432 50 L18.824 56 Z" fill="white" fill-opacity="0.09"/>
<path d="M50 74 L39.608 68 L39.608 80 L50 86 Z" fill="white" fill-opacity="0.14"/>
<path d="M39.608 68 L29.216 62 L29.216 74 L39.608 80 Z" fill="white" fill-opacity="0.11"/>
<path d="M29.216 62 L18.824 56 L18.824 68 L29.216 74 Z" fill="white" fill-opacity="0.16"/>
<path d="M18.824 56 L8.432 50 L8.432 62 L18.824 68 Z" fill="white" fill-opacity="0.05"/>
<path d="M50 86 L39.608 80 L39.608 92 L50 98 Z" fill="white" fill-opacity="0.19"/>
<path d="M39.608 80 L29.216 74 L29.216 86 L39.608 92 Z" fill="white" fill-opacity="0.07"/>
<path d="M29.216 74 L18.824 68 L18.824 80 L29.216 86 Z" fill="white" fill-opacity="0.12"/>
<path d="M18.824 68 L8.432 62 L8.432 74 L18.824 80 Z" fill="white" fill-opacity="0.04"/>
<!-- Right face -->
<path d="M50 50 L91.568 26 L91.568 74 L50 98 Z" fill="white" fill-opacity="0.15" stroke="white" stroke-width="2" stroke-linejoin="round"/>
<!-- Right face grid cells -->
<path d="M50 50 L60.392 44 L60.392 56 L50 62 Z" fill="white" fill-opacity="0.11"/>
<path d="M60.392 44 L70.784 38 L70.784 50 L60.392 56 Z" fill="white" fill-opacity="0.05"/>
<path d="M70.784 38 L81.176 32 L81.176 44 L70.784 50 Z" fill="white" fill-opacity="0.16"/>
<path d="M81.176 32 L91.568 26 L91.568 38 L81.176 44 Z" fill="white" fill-opacity="0.09"/>
<path d="M50 62 L60.392 56 L60.392 68 L50 74 Z" fill="white" fill-opacity="0.18"/>
<path d="M60.392 56 L70.784 50 L70.784 62 L60.392 68 Z" fill="white" fill-opacity="0.12"/>
<path d="M70.784 50 L81.176 44 L81.176 56 L70.784 62 Z" fill="white" fill-opacity="0.04"/>
<path d="M81.176 44 L91.568 38 L91.568 50 L81.176 56 Z" fill="white" fill-opacity="0.14"/>
<path d="M50 74 L60.392 68 L60.392 80 L50 86 Z" fill="white" fill-opacity="0.07"/>
<path d="M60.392 68 L70.784 62 L70.784 74 L60.392 80 Z" fill="white" fill-opacity="0.19"/>
<path d="M70.784 62 L81.176 56 L81.176 68 L70.784 74 Z" fill="white" fill-opacity="0.11"/>
<path d="M81.176 56 L91.568 50 L91.568 62 L81.176 68 Z" fill="white" fill-opacity="0.05"/>
<path d="M50 86 L60.392 80 L60.392 92 L50 98 Z" fill="white" fill-opacity="0.16"/>
<path d="M60.392 80 L70.784 74 L70.784 86 L60.392 92 Z" fill="white" fill-opacity="0.09"/>
<path d="M70.784 74 L81.176 68 L81.176 80 L70.784 86 Z" fill="white" fill-opacity="0.14"/>
<path d="M81.176 68 L91.568 62 L91.568 74 L81.176 80 Z" fill="white" fill-opacity="0.07"/>
<!-- Top face - open -->
<path d="M50 2 L8.432 26 L50 50 L91.568 26 Z" fill="none" stroke="white" stroke-width="2" stroke-linejoin="round"/>
<!-- OpenCode logo on top face -->
<g transform="matrix(0.866, 0.5, -0.866, 0.5, 50, 26) scale(0.75)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-16 -20 L16 -20 L16 20 L-16 20 Z M-8 -12 L-8 12 L8 12 L8 -12 Z" fill="white"/>
<path d="M-8 -4 L8 -4 L8 12 L-8 12 Z" fill="white" fill-opacity="0.4"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

@@ -1,47 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Left face -->
<path d="M50 50 L8.432 26 L8.432 74 L50 98 Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="2" stroke-linejoin="round"/>
<!-- Left face grid cells -->
<path d="M50 50 L39.608 44 L39.608 56 L50 62 Z" fill="black" fill-opacity="0.07"/>
<path d="M39.608 44 L29.216 38 L29.216 50 L39.608 56 Z" fill="black" fill-opacity="0.16"/>
<path d="M29.216 38 L18.824 32 L18.824 44 L29.216 50 Z" fill="black" fill-opacity="0.05"/>
<path d="M18.824 32 L8.432 26 L8.432 38 L18.824 44 Z" fill="black" fill-opacity="0.19"/>
<path d="M50 62 L39.608 56 L39.608 68 L50 74 Z" fill="black" fill-opacity="0.12"/>
<path d="M39.608 56 L29.216 50 L29.216 62 L39.608 68 Z" fill="black" fill-opacity="0.04"/>
<path d="M29.216 50 L18.824 44 L18.824 56 L29.216 62 Z" fill="black" fill-opacity="0.18"/>
<path d="M18.824 44 L8.432 38 L8.432 50 L18.824 56 Z" fill="black" fill-opacity="0.09"/>
<path d="M50 74 L39.608 68 L39.608 80 L50 86 Z" fill="black" fill-opacity="0.14"/>
<path d="M39.608 68 L29.216 62 L29.216 74 L39.608 80 Z" fill="black" fill-opacity="0.11"/>
<path d="M29.216 62 L18.824 56 L18.824 68 L29.216 74 Z" fill="black" fill-opacity="0.16"/>
<path d="M18.824 56 L8.432 50 L8.432 62 L18.824 68 Z" fill="black" fill-opacity="0.05"/>
<path d="M50 86 L39.608 80 L39.608 92 L50 98 Z" fill="black" fill-opacity="0.19"/>
<path d="M39.608 80 L29.216 74 L29.216 86 L39.608 92 Z" fill="black" fill-opacity="0.07"/>
<path d="M29.216 74 L18.824 68 L18.824 80 L29.216 86 Z" fill="black" fill-opacity="0.12"/>
<path d="M18.824 68 L8.432 62 L8.432 74 L18.824 80 Z" fill="black" fill-opacity="0.04"/>
<!-- Right face -->
<path d="M50 50 L91.568 26 L91.568 74 L50 98 Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="2" stroke-linejoin="round"/>
<!-- Right face grid cells -->
<path d="M50 50 L60.392 44 L60.392 56 L50 62 Z" fill="black" fill-opacity="0.11"/>
<path d="M60.392 44 L70.784 38 L70.784 50 L60.392 56 Z" fill="black" fill-opacity="0.05"/>
<path d="M70.784 38 L81.176 32 L81.176 44 L70.784 50 Z" fill="black" fill-opacity="0.16"/>
<path d="M81.176 32 L91.568 26 L91.568 38 L81.176 44 Z" fill="black" fill-opacity="0.09"/>
<path d="M50 62 L60.392 56 L60.392 68 L50 74 Z" fill="black" fill-opacity="0.18"/>
<path d="M60.392 56 L70.784 50 L70.784 62 L60.392 68 Z" fill="black" fill-opacity="0.12"/>
<path d="M70.784 50 L81.176 44 L81.176 56 L70.784 62 Z" fill="black" fill-opacity="0.04"/>
<path d="M81.176 44 L91.568 38 L91.568 50 L81.176 56 Z" fill="black" fill-opacity="0.14"/>
<path d="M50 74 L60.392 68 L60.392 80 L50 86 Z" fill="black" fill-opacity="0.07"/>
<path d="M60.392 68 L70.784 62 L70.784 74 L60.392 80 Z" fill="black" fill-opacity="0.19"/>
<path d="M70.784 62 L81.176 56 L81.176 68 L70.784 74 Z" fill="black" fill-opacity="0.11"/>
<path d="M81.176 56 L91.568 50 L91.568 62 L81.176 68 Z" fill="black" fill-opacity="0.05"/>
<path d="M50 86 L60.392 80 L60.392 92 L50 98 Z" fill="black" fill-opacity="0.16"/>
<path d="M60.392 80 L70.784 74 L70.784 86 L60.392 92 Z" fill="black" fill-opacity="0.09"/>
<path d="M70.784 74 L81.176 68 L81.176 80 L70.784 86 Z" fill="black" fill-opacity="0.14"/>
<path d="M81.176 68 L91.568 62 L91.568 74 L81.176 80 Z" fill="black" fill-opacity="0.07"/>
<!-- Top face - open -->
<path d="M50 2 L8.432 26 L50 50 L91.568 26 Z" fill="none" stroke="black" stroke-width="2" stroke-linejoin="round"/>
<!-- OpenCode logo on top face -->
<g transform="matrix(0.866, 0.5, -0.866, 0.5, 50, 26) scale(0.75)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-16 -20 L16 -20 L16 20 L-16 20 Z M-8 -12 L-8 12 L8 12 L8 -12 Z" fill="black"/>
<path d="M-8 -4 L8 -4 L8 12 L-8 12 Z" fill="black" fill-opacity="0.4"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 973 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 971 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 722 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1001 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 KiB

-23
View File
@@ -1,23 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
export default tseslint.config([
globalIgnores(['dist', '.openchamber']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
-81
View File
@@ -1,81 +0,0 @@
#!/usr/bin/env node
/**
* Fix for http-proxy package util._extend deprecation warning
* This script patches the http-proxy package to use Object.assign instead of util._extend
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function fixHttpProxyDeprecation() {
try {
// Find the http-proxy package in node_modules
const httpProxyDir = path.join(__dirname, 'node_modules', 'http-proxy', 'lib', 'http-proxy');
const indexPath = path.join(httpProxyDir, 'index.js');
const commonPath = path.join(httpProxyDir, 'common.js');
if (!fs.existsSync(indexPath) || !fs.existsSync(commonPath)) {
return;
}
// Patch index.js
let needsPatch = false;
if (fs.existsSync(indexPath)) {
let content = fs.readFileSync(indexPath, 'utf8');
let indexPatched = false;
if (content.includes("require('util')._extend")) {
content = content.replace(
/extend\s*=\s*require\('util'\)\._extend,/,
"extend = Object.assign,"
);
indexPatched = true;
}
if (content.includes("require('util').inherits")) {
content = content.replace(
/require\('util'\)\.inherits\((\w+),\s*(\w+)\);/,
"Object.setPrototypeOf($1.prototype, $2.prototype);"
);
indexPatched = true;
}
if (indexPatched) {
fs.writeFileSync(indexPath, content, 'utf8');
needsPatch = true;
}
}
// Patch common.js
if (fs.existsSync(commonPath)) {
let content = fs.readFileSync(commonPath, 'utf8');
let commonPatched = false;
if (content.includes("require('util')._extend")) {
content = content.replace(
/extend\s*=\s*require\('util'\)\._extend,/,
"extend = Object.assign,"
);
commonPatched = true;
}
if (commonPatched) {
fs.writeFileSync(commonPath, content, 'utf8');
needsPatch = true;
}
}
} catch (error) {
// Silently handle errors - functionality is not affected
}
}
// Run the fix
fixHttpProxyDeprecation();
-161
View File
@@ -1,161 +0,0 @@
{
"name": "openchamber-monorepo",
"version": "1.9.9",
"description": "OpenChamber monorepo workspace for web, ui, and desktop runtimes",
"private": true,
"type": "module",
"packageManager": "bun@1.3.5",
"workspaces": [
"packages/*"
],
"engines": {
"node": ">=20.0.0"
},
"keywords": [
"opencode",
"ai",
"coding",
"openchamber",
"cli"
],
"author": "Bohdan Triapitsyn",
"license": "MIT",
"scripts": {
"dev": "concurrently -n \"server,web,ui\" -c \"cyan,magenta,yellow\" \"bun run --cwd packages/web dev:server:watch\" \"bun run --cwd packages/web build:watch\" \"bun run --cwd packages/ui dev\"",
"build": "bun run --filter '*' build",
"build:web": "bun run --cwd packages/web build",
"build:ui": "bun run --cwd packages/ui build",
"build:desktop": "bun run --cwd packages/desktop build",
"build:electron": "bun run --cwd packages/electron build",
"type-check": "bun run --filter '*' type-check",
"type-check:web": "bun run --cwd packages/web type-check",
"type-check:ui": "bun run --cwd packages/ui type-check",
"type-check:desktop": "bun run --cwd packages/desktop type-check",
"type-check:electron": "bun run --cwd packages/electron type-check",
"lint": "bun run --filter '*' lint",
"lint:web": "bun run --cwd packages/web lint",
"lint:ui": "bun run --cwd packages/ui lint",
"lint:desktop": "bun run --cwd packages/desktop lint",
"lint:electron": "bun run --cwd packages/electron lint",
"clean": "bun run --filter '*' clean",
"postinstall": "patch-package",
"dev:web": "bun run --cwd packages/web build:watch",
"dev:web:server": "bun run --cwd packages/web dev:server:watch",
"dev:web:full": "node ./scripts/dev-web-full.mjs",
"dev:web:hmr": "node ./scripts/dev-web-hmr.mjs",
"start:web": "bun run --cwd packages/web start",
"pack:web": "bun pm pack --cwd packages/web",
"desktop:start-cli": "node ./packages/desktop/scripts/opencode-cli.mjs start",
"desktop:stop-cli": "node ./packages/desktop/scripts/opencode-cli.mjs stop",
"desktop:dev": "node ./packages/desktop/scripts/desktop-dev.mjs",
"desktop:build": "bun run --cwd packages/desktop build:sidecar && bun run --cwd packages/desktop tauri build",
"electron:dev": "node ./packages/electron/scripts/electron-dev.mjs",
"electron:build": "bun run --cwd packages/electron package",
"desktop:lint": "bun run --cwd packages/desktop lint && cargo fmt --manifest-path packages/desktop/src-tauri/Cargo.toml -- --check && cargo clippy --manifest-path packages/desktop/src-tauri/Cargo.toml -- -D warnings",
"desktop:type-check": "bun run --cwd packages/desktop type-check && cargo fmt --manifest-path packages/desktop/src-tauri/Cargo.toml -- --check && cargo clippy --manifest-path packages/desktop/src-tauri/Cargo.toml -- -D warnings",
"vscode:dev": "node ./scripts/dev-vscode.mjs",
"vscode:build": "bun run --cwd packages/vscode build",
"vscode:package": "bun run --cwd packages/vscode package",
"vscode:type-check": "bun run --cwd packages/vscode type-check",
"docs:validate": "node scripts/docs/validate-docs.mjs",
"icons:sprite": "node scripts/generate-file-type-sprite.mjs",
"themes:port:opencode": "tsx scripts/port-opencode-theme.ts",
"version:bump": "node scripts/bump-version.mjs",
"release:prepare": "bun run build && bun run type-check && bun run lint",
"release:test": "./scripts/test-release-build.sh",
"release:test:intel": "./scripts/test-release-build.sh x86_64",
"release:test:arm": "./scripts/test-release-build.sh aarch64"
},
"dependencies": {
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-rust": "^6.0.2",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "6.12.2",
"@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "6.39.13",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/ibm-plex-sans": "^5.1.1",
"@heroui/scroll-shadow": "^2.3.18",
"@heroui/system": "^2.4.23",
"@heroui/theme": "^2.4.23",
"@ibm/plex": "^6.4.1",
"@lezer/highlight": "^1.2.3",
"@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "^1.4.25",
"@base-ui/react": "^1.4.0",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.8",
"@remixicon/react": "^4.7.0",
"@types/react-syntax-highlighter": "^15.5.13",
"bun-pty": "^0.4.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"express": "^5.1.0",
"ghostty-web": "0.3.0",
"http-proxy-middleware": "^3.0.5",
"next-themes": "^0.4.6",
"node-pty": "1.2.0-beta.12",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.6.6",
"remark-gfm": "^4.0.1",
"simple-git": "^3.28.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"yaml": "^2.8.1",
"zod": "^4.3.6",
"zustand": "^5.0.8"
},
"overrides": {
"@codemirror/language": "6.12.2",
"@codemirror/view": "6.39.13"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@tailwindcss/postcss": "^4.0.0",
"@types/dom-speech-recognition": "^0.0.7",
"@types/node": "^24.3.1",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.21",
"babel-plugin-react-compiler": "^1.0.0",
"concurrently": "^9.2.1",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"nodemon": "^3.1.7",
"patch-package": "^8.0.0",
"tailwindcss": "^4.0.0",
"tsx": "^4.20.6",
"tw-animate-css": "^1.3.8",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"
}
}
-21
View File
@@ -1,21 +0,0 @@
# Vite build output
dist/
# Tauri build artifacts
src-tauri/target/
# Tauri generated code
src-tauri/gen/
# Desktop sidecar + bundled web assets (generated)
src-tauri/resources/web-dist/
src-tauri/sidecars/openchamber-server-*
src-tauri/sidecars/*.exe
!src-tauri/resources/.gitkeep
!src-tauri/sidecars/.gitkeep
# OpenCode CLI state tracking
.opencode-cli-state.json
# OS-specific
.DS_Store
-68
View File
@@ -1,68 +0,0 @@
# <picture><source media="(prefers-color-scheme: dark)" srcset="https://github.com/btriapitsyn/openchamber/raw/HEAD/docs/references/badges/openchamber-logo-dark.svg"><img src="https://github.com/btriapitsyn/openchamber/raw/HEAD/docs/references/badges/openchamber-logo-light.svg" width="32" height="32" align="absmiddle" /></picture> OpenChamber Desktop
[![GitHub stars](https://img.shields.io/github/stars/btriapitsyn/openchamber?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iI2YxZWNlYyIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0yMjkuMDYsMTA4Ljc5bC00OC43LDQyLDE0Ljg4LDYyLjc5YTguNCw4LjQsMCwwLDEtMTIuNTIsOS4xN0wxMjgsMTg5LjA5LDczLjI4LDIyMi43NGE4LjQsOC40LDAsMCwxLTEyLjUyLTkuMTdsMTQuODgtNjIuNzktNDguNy00MkE4LjQ2LDguNDYsMCwwLDEsMzEuNzMsOTRMOTUuNjQsODguOGwyNC42Mi01OS42YTguMzYsOC4zNiwwLDAsMSwxNS40OCwwbDI0LjYyLDU5LjZMMjI0LjI3LDk0QTguNDYsOC40NiwwLDAsMSwyMjkuMDYsMTA4Ljc5WiIgb3BhY2l0eT0iMC4yIj48L3BhdGg%2BPHBhdGggZD0iTTIzOS4xOCw5Ny4yNkExNi4zOCwxNi4zOCwwLDAsMCwyMjQuOTIsODZsLTU5LTQuNzZMMTQzLjE0LDI2LjE1YTE2LjM2LDE2LjM2LDAsMCwwLTMwLjI3LDBMOTAuMTEsODEuMjMsMzEuMDgsODZhMTYuNDYsMTYuNDYsMCwwLDAtOS4zNywyOC44Nmw0NSwzOC44M0w1MywyMTEuNzVhMTYuMzgsMTYuMzgsMCwwLDAsMjQuNSwxNy44MkwxMjgsMTk4LjQ5bDUwLjUzLDMxLjA4QTE2LjQsMTYuNCwwLDAsMCwyMDMsMjExLjc1bC0xMy43Ni01OC4wNyw0NS0zOC44M0ExNi40MywxNi40MywwLDAsMCwyMzkuMTgsOTcuMjZabS0xNS4zNCw1LjQ3LTQ4LjcsNDJhOCw4LDAsMCwwLTIuNTYsNy45MWwxNC44OCw2Mi44YS4zNy4zNywwLDAsMS0uMTcuNDhjLS4xOC4xNC0uMjMuMTEtLjM4LDBsLTU0LjcyLTMzLjY1YTgsOCwwLDAsMC04LjM4LDBMNjkuMDksMjE1Ljk0Yy0uMTUuMDktLjE5LjEyLS4zOCwwYS4zNy4zNywwLDAsMS0uMTctLjQ4bDE0Ljg4LTYyLjhhOCw4LDAsMCwwLTIuNTYtNy45MWwtNDguNy00MmMtLjEyLS4xLS4yMy0uMTktLjEzLS41cy4xOC0uMjcuMzMtLjI5bDYzLjkyLTUuMTZBOCw4LDAsMCwwLDEwMyw5MS44NmwyNC42Mi01OS42MWMuMDgtLjE3LjExLS4yNS4zNS0uMjVzLjI3LjA4LjM1LjI1TDE1Myw5MS44NmE4LDgsMCwwLDAsNi43NSw0LjkybDYzLjkyLDUuMTZjLjE1LDAsLjI0LDAsLjMzLjI5UzIyNCwxMDIuNjMsMjIzLjg0LDEwMi43M1oiPjwvcGF0aD48L3N2Zz4%3D&logoColor=FFFCF0&labelColor=100F0F&color=66800B)](https://github.com/btriapitsyn/openchamber/stargazers)
[![GitHub release](https://img.shields.io/github/v/release/btriapitsyn/openchamber?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iI2YxZWNlYyIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0xMjgsMTI5LjA5VjIzMmE4LDgsMCwwLDEtMy44NC0xbC04OC00OC4xOGE4LDgsMCwwLDEtNC4xNi03VjgwLjE4YTgsOCwwLDAsMSwuNy0zLjI1WiIgb3BhY2l0eT0iMC4yIj48L3BhdGg%2BPHBhdGggZD0iTTIyMy42OCw2Ni4xNSwxMzUuNjgsMThhMTUuODgsMTUuODgsMCwwLDAtMTUuMzYsMGwtODgsNDguMTdhMTYsMTYsMCwwLDAtOC4zMiwxNHY5NS42NGExNiwxNiwwLDAsMCw4LjMyLDE0bDg4LDQ4LjE3YTE1Ljg4LDE1Ljg4LDAsMCwwLDE1LjM2LDBsODgtNDguMTdhMTYsMTYsMCwwLDAsOC4zMi0xNFY4MC4xOEExNiwxNiwwLDAsMCwyMjMuNjgsNjYuMTVaTTEyOCwzMmw4MC4zNCw0NC0yOS43NywxNi4zLTgwLjM1LTQ0Wk0xMjgsMTIwLDQ3LjY2LDc2bDMzLjktMTguNTYsODAuMzQsNDRaTTQwLDkwbDgwLDQzLjc4djg1Ljc5TDQwLDE3NS44MlptMTc2LDg1Ljc4aDBsLTgwLDQzLjc5VjEzMy44MmwzMi0xNy41MVYxNTJhOCw4LDAsMCwwLDE2LDBWMTA3LjU1TDIxNiw5MHY4NS43N1oiPjwvcGF0aD48L3N2Zz4%3D&logoColor=FFFCF0&labelColor=100F0F&color=205EA6)](https://github.com/btriapitsyn/openchamber/releases/latest)
[![Discord](https://img.shields.io/badge/Discord-join.svg?style=flat&labelColor=100F0F&color=8B7EC8&logo=discord&logoColor=FFFCF0)](https://discord.gg/ZYRSdnwwKA)
A native macOS app for [OpenCode](https://opencode.ai). Feels like home - multiple windows, SSH remotes, project actions, and everything running locally.
Full project overview, screenshots, and all features: [github.com/btriapitsyn/openchamber](https://github.com/btriapitsyn/openchamber)
## Install
Download from [Releases](https://github.com/btriapitsyn/openchamber/releases). Available for macOS (Apple Silicon and Intel).
> **Prerequisite:** [OpenCode CLI](https://opencode.ai) installed.
## What makes the desktop app special
- **Remote instances over SSH** - connect to remote OpenChamber servers with dedicated lifecycle and UX flows
- **Project Actions** - run dev servers, configure SSH port forwarding, open remote URLs locally
- **Multi-window** - work on several projects in parallel, each in its own window
- **"Open In" shortcuts** - open workspace in Finder, Terminal, or your editor of choice
- **Local + remote switching** - jump between local and remote OpenChamber instances
- **Native macOS integration** - menus, deep-links, auto-update, and polished window management
Plus everything from the shared OpenChamber UI: branchable timeline, Git sidebar, terminal, voice mode, and more.
## Features
### Core UI
- Branchable chat timeline with `/undo`, `/redo`, and one-click forks from earlier turns
- Smart tool UIs for diffs, file operations, permissions, and long-running task progress
- Multi-agent runs from one prompt with isolated worktrees for safe comparisons
- Git workflows in-app: identities, commits, PR creation, checks, and merge actions
- Context visibility tools (token/cost breakdowns, raw message inspection, and activity summaries)
- Integrated terminal with per-directory sessions and stable performance on heavy output
### Desktop (macOS)
- Native macOS menu integration with polished app actions and deep-link handling
- Multi-window support for parallel project/session workflows
- "Open In" shortcuts for Finder, Terminal, and your preferred editor
- Fast switching between local and remote instances
- Workspace-first startup flow with directory picker and steadier window restore behavior
### Remote Tunnel (Desktop)
- Configure in **Settings -> OpenChamber -> Remote Tunnel**.
- Supported Cloudflare modes: **Quick**, **Managed Remote**, **Managed Local**.
- One active tunnel per Desktop instance. Starting a different mode replaces the current tunnel.
- Replacing or stopping a tunnel revokes existing connect links and invalidates remote tunnel sessions.
- Connect links are one-time tokens; generate a new link for each new connection attempt.
## Development
```bash
git clone https://github.com/btriapitsyn/openchamber.git
cd openchamber
bun install
bun run desktop:dev
```
</details>
## License
MIT
-110
View File
@@ -1,110 +0,0 @@
<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenChamber</title>
<style>
:root {
--splash-background-dark: #151313;
--splash-stroke-dark: #CECDC3;
--splash-background-light: #FFFCF0;
--splash-stroke-light: #100F0F;
--splash-background: var(--splash-background-dark);
--splash-stroke: var(--splash-stroke-dark);
--splash-face-fill: rgba(255, 255, 255, 0.15);
--splash-cell-fill: rgba(255, 255, 255, 0.35);
--splash-logo-fill: var(--splash-stroke);
}
html[data-splash-variant='light'] {
--splash-background: var(--splash-background-light);
--splash-stroke: var(--splash-stroke-light);
--splash-face-fill: rgba(0, 0, 0, 0.15);
--splash-cell-fill: rgba(0, 0, 0, 0.4);
}
html[data-splash-variant='dark'] {
--splash-background: var(--splash-background-dark);
--splash-stroke: var(--splash-stroke-dark);
}
@supports (color: color-mix(in srgb, white 50%, transparent)) {
:root {
--splash-face-fill: color-mix(in srgb, var(--splash-stroke) 15%, transparent);
--splash-cell-fill: color-mix(in srgb, var(--splash-stroke) 35%, transparent);
}
}
@media (prefers-color-scheme: light) {
:root {
--splash-background: var(--splash-background-light);
--splash-stroke: var(--splash-stroke-light);
--splash-face-fill: rgba(0, 0, 0, 0.15);
--splash-cell-fill: rgba(0, 0, 0, 0.4);
}
}
html,
body {
margin: 0;
height: 100%;
}
body {
background-color: var(--splash-background);
color: var(--splash-stroke);
display: flex;
align-items: center;
justify-content: center;
font-family: system-ui, -apple-system, sans-serif;
}
</style>
</head>
<body>
<noscript>OpenChamber requires JavaScript.</noscript>
<svg width="120" height="120" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OpenChamber loading icon">
<path d="M50 50 L8.432 26 L8.432 74 L50 98 Z" fill="var(--splash-face-fill)" stroke="var(--splash-stroke)" stroke-width="2" stroke-linejoin="round"/>
<path d="M50 50 L39.608 44 L39.608 56 L50 62 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M39.608 44 L29.216 38 L29.216 50 L39.608 56 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
<path d="M29.216 38 L18.824 32 L18.824 44 L29.216 50 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
<path d="M18.824 32 L8.432 26 L8.432 38 L18.824 44 Z" fill="var(--splash-cell-fill)" opacity="0.55"/>
<path d="M50 62 L39.608 56 L39.608 68 L50 74 Z" fill="var(--splash-cell-fill)" opacity="0.35"/>
<path d="M39.608 56 L29.216 50 L29.216 62 L39.608 68 Z" fill="var(--splash-cell-fill)" opacity="0.1"/>
<path d="M29.216 50 L18.824 44 L18.824 56 L29.216 62 Z" fill="var(--splash-cell-fill)" opacity="0.5"/>
<path d="M18.824 44 L8.432 38 L8.432 50 L18.824 56 Z" fill="var(--splash-cell-fill)" opacity="0.25"/>
<path d="M50 74 L39.608 68 L39.608 80 L50 86 Z" fill="var(--splash-cell-fill)" opacity="0.4"/>
<path d="M39.608 68 L29.216 62 L29.216 74 L39.608 80 Z" fill="var(--splash-cell-fill)" opacity="0.3"/>
<path d="M29.216 62 L18.824 56 L18.824 68 L29.216 74 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
<path d="M18.824 56 L8.432 50 L8.432 62 L18.824 68 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
<path d="M50 86 L39.608 80 L39.608 92 L50 98 Z" fill="var(--splash-cell-fill)" opacity="0.55"/>
<path d="M39.608 80 L29.216 74 L29.216 86 L39.608 92 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M29.216 74 L18.824 68 L18.824 80 L29.216 86 Z" fill="var(--splash-cell-fill)" opacity="0.35"/>
<path d="M18.824 68 L8.432 62 L8.432 74 L18.824 80 Z" fill="var(--splash-cell-fill)" opacity="0.1"/>
<path d="M50 50 L91.568 26 L91.568 74 L50 98 Z" fill="var(--splash-face-fill)" stroke="var(--splash-stroke)" stroke-width="2" stroke-linejoin="round"/>
<path d="M50 50 L60.392 44 L60.392 56 L50 62 Z" fill="var(--splash-cell-fill)" opacity="0.3"/>
<path d="M60.392 44 L70.784 38 L70.784 50 L60.392 56 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
<path d="M70.784 38 L81.176 32 L81.176 44 L70.784 50 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
<path d="M81.176 32 L91.568 26 L91.568 38 L81.176 44 Z" fill="var(--splash-cell-fill)" opacity="0.25"/>
<path d="M50 62 L60.392 56 L60.392 68 L50 74 Z" fill="var(--splash-cell-fill)" opacity="0.5"/>
<path d="M60.392 56 L70.784 50 L70.784 62 L60.392 68 Z" fill="var(--splash-cell-fill)" opacity="0.35"/>
<path d="M70.784 50 L81.176 44 L81.176 56 L70.784 62 Z" fill="var(--splash-cell-fill)" opacity="0.1"/>
<path d="M81.176 44 L91.568 38 L91.568 50 L81.176 56 Z" fill="var(--splash-cell-fill)" opacity="0.4"/>
<path d="M50 74 L60.392 68 L60.392 80 L50 86 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M60.392 68 L70.784 62 L70.784 74 L60.392 80 Z" fill="var(--splash-cell-fill)" opacity="0.55"/>
<path d="M70.784 62 L81.176 56 L81.176 68 L70.784 74 Z" fill="var(--splash-cell-fill)" opacity="0.3"/>
<path d="M81.176 56 L91.568 50 L91.568 62 L81.176 68 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
<path d="M50 86 L60.392 80 L60.392 92 L50 98 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
<path d="M60.392 80 L70.784 74 L70.784 86 L60.392 92 Z" fill="var(--splash-cell-fill)" opacity="0.25"/>
<path d="M70.784 74 L81.176 68 L81.176 80 L70.784 86 Z" fill="var(--splash-cell-fill)" opacity="0.4"/>
<path d="M81.176 68 L91.568 62 L91.568 74 L81.176 80 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M50 2 L8.432 26 L50 50 L91.568 26 Z" fill="none" stroke="var(--splash-stroke)" stroke-width="2" stroke-linejoin="round"/>
<g transform="matrix(0.866, 0.5, -0.866, 0.5, 50, 26) scale(0.75)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-16 -20 L16 -20 L16 20 L-16 20 Z M-8 -12 L-8 12 L8 12 L8 -12 Z" fill="var(--splash-logo-fill)"/>
<path d="M-8 -4 L8 -4 L8 12 L-8 12 Z" fill="var(--splash-logo-fill)" fill-opacity="0.4"/>
</g>
</svg>
</body>
</html>
-26
View File
@@ -1,26 +0,0 @@
{
"name": "@openchamber/desktop",
"version": "1.9.9",
"private": true,
"type": "module",
"desktopPrerequisites": [
"Rust stable toolchain (via rustup)",
"Xcode Command Line Tools installed",
"Tauri CLI installed (cargo install tauri-cli@^2)"
],
"scripts": {
"tauri": "tauri",
"tauri:dev": "tauri dev --features devtools",
"tauri:build": "tauri build",
"build:sidecar": "node ./scripts/build-sidecar.mjs",
"build": "bun -e \"process.exit(0)\"",
"type-check": "bun -e \"process.exit(0)\"",
"lint": "bun -e \"process.exit(0)\""
},
"dependencies": {},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@types/node": "^24.3.1",
"typescript": "~5.8.3"
}
}
@@ -1,135 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '..', '..', '..');
const webDir = path.join(repoRoot, 'packages', 'web');
const desktopTauriDir = path.join(repoRoot, 'packages', 'desktop', 'src-tauri');
const resourcesDir = path.join(desktopTauriDir, 'resources');
const resourcesWebDistDir = path.join(resourcesDir, 'web-dist');
const webDistDir = path.join(webDir, 'dist');
const sidecarsDir = path.join(desktopTauriDir, 'sidecars');
const inferTargetTriple = () => {
if (typeof process.env.TAURI_ENV_TARGET_TRIPLE === 'string' && process.env.TAURI_ENV_TARGET_TRIPLE.trim()) {
return process.env.TAURI_ENV_TARGET_TRIPLE.trim();
}
if (process.platform === 'darwin') {
return process.arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin';
}
if (process.platform === 'win32') {
return 'x86_64-pc-windows-msvc';
}
if (process.platform === 'linux') {
return process.arch === 'arm64' ? 'aarch64-unknown-linux-gnu' : 'x86_64-unknown-linux-gnu';
}
return `${process.arch}-${process.platform}`;
};
const targetTriple = inferTargetTriple();
const bunCompileTargetByTriple = {
'aarch64-apple-darwin': 'bun-darwin-arm64',
'x86_64-apple-darwin': 'bun-darwin-x64',
'aarch64-unknown-linux-gnu': 'bun-linux-arm64',
'x86_64-unknown-linux-gnu': 'bun-linux-x64',
'x86_64-pc-windows-msvc': 'bun-windows-x64',
};
const compileTarget = bunCompileTargetByTriple[targetTriple];
if (!compileTarget) {
console.warn(
`[desktop] unknown target triple '${targetTriple}', falling back to host-arch sidecar build`
);
}
const sidecarBaseName = process.platform === 'win32'
? `openchamber-server-${targetTriple}.exe`
: `openchamber-server-${targetTriple}`;
const sidecarOutPath = path.join(sidecarsDir, sidecarBaseName);
const run = (cmd, args, cwd) => {
const result = spawnSync(cmd, args, { cwd, stdio: 'inherit' });
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`Command failed: ${cmd} ${args.join(' ')}`);
}
};
const resolveBun = () => {
if (typeof process.env.BUN === 'string' && process.env.BUN.trim()) {
return process.env.BUN.trim();
}
const result = spawnSync('/bin/bash', ['-lc', 'command -v bun'], { encoding: 'utf8' });
const resolved = (result.stdout || '').trim();
if (resolved) {
return resolved;
}
return 'bun';
};
const bunExe = resolveBun();
const copyDir = async (src, dst) => {
await fs.mkdir(dst, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const from = path.join(src, entry.name);
const to = path.join(dst, entry.name);
if (entry.isDirectory()) {
await copyDir(from, to);
} else if (entry.isSymbolicLink()) {
const link = await fs.readlink(from);
await fs.symlink(link, to);
} else {
await fs.copyFile(from, to);
}
}
};
console.log('[desktop] building web UI dist...');
run(bunExe, ['run', 'build'], webDir);
console.log('[desktop] preparing tauri resources...');
await fs.mkdir(resourcesDir, { recursive: true });
await fs.rm(resourcesWebDistDir, { recursive: true, force: true });
await copyDir(webDistDir, resourcesWebDistDir);
console.log('[desktop] building openchamber-server sidecar...');
await fs.mkdir(sidecarsDir, { recursive: true });
const buildArgs = [
'build',
'--compile',
path.join(webDir, 'server', 'index.js'),
'--outfile',
sidecarOutPath,
];
if (compileTarget) {
buildArgs.push('--target', compileTarget);
}
run(bunExe, buildArgs, repoRoot);
if (process.platform !== 'win32') {
await fs.chmod(sidecarOutPath, 0o755);
}
console.log(`[desktop] sidecar ready: ${sidecarOutPath}`);
console.log(`[desktop] web assets ready: ${resourcesWebDistDir}`);
@@ -1,138 +0,0 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '../../..');
const desktopDir = path.join(repoRoot, 'packages/desktop');
function spawnProcess(command, args, opts = {}) {
return spawn(command, args, {
cwd: repoRoot,
env: { ...process.env },
stdio: 'inherit',
detached: process.platform !== 'win32',
...opts,
});
}
function waitForExit(child, timeoutMs) {
return new Promise((resolve) => {
if (!child || child.exitCode !== null || child.signalCode !== null) {
resolve();
return;
}
const onExit = () => {
clearTimeout(timer);
resolve();
};
const timer = setTimeout(() => {
child.off('exit', onExit);
resolve();
}, timeoutMs);
child.once('exit', onExit);
});
}
function signalChild(child, signal) {
if (!child || child.exitCode !== null || child.signalCode !== null) {
return;
}
try {
if (process.platform !== 'win32') {
process.kill(-child.pid, signal);
return;
}
} catch {
}
try {
child.kill(signal);
} catch {
}
}
async function stopChildTree(child) {
if (!child || child.exitCode !== null || child.signalCode !== null) {
return;
}
signalChild(child, 'SIGINT');
await waitForExit(child, 2500);
if (child.exitCode === null && child.signalCode === null) {
signalChild(child, 'SIGTERM');
await waitForExit(child, 2500);
}
if (child.exitCode === null && child.signalCode === null) {
signalChild(child, 'SIGKILL');
await waitForExit(child, 1000);
}
}
async function main() {
const tauriProcess = spawnProcess('bun', [
'--cwd',
desktopDir,
'tauri',
'dev',
'--features',
'devtools',
'--config',
'./src-tauri/tauri.dev.conf.json',
]);
let cleaning = false;
const teardown = async (code) => {
if (cleaning) {
return;
}
cleaning = true;
await stopChildTree(tauriProcess);
process.exit(typeof code === 'number' ? code : 0);
};
const handleChildExit = (childName) => (code, signal) => {
if (code !== 0 || signal) {
console.warn(`[desktop:dev] ${childName} exited with code ${code ?? 'null'} signal ${signal ?? 'none'}.`);
}
teardown(code).catch((error) => {
console.error('[desktop:dev] Cleanup error:', error);
process.exit(code ?? 1);
});
};
tauriProcess.on('exit', handleChildExit('Tauri dev process'));
const errorHandler = (label) => (error) => {
console.error(`[desktop:dev] Failed to start ${label}:`, error);
teardown(1).catch(() => process.exit(1));
};
tauriProcess.on('error', errorHandler('Tauri dev process'));
const signalExitCodes = {
SIGINT: 130,
SIGTERM: 143,
SIGQUIT: 131,
};
Object.entries(signalExitCodes).forEach(([signal, exitCode]) => {
process.on(signal, () => {
teardown(exitCode).catch(() => process.exit(exitCode));
});
});
}
main().catch((error) => {
console.error('[desktop:dev] Unexpected error:', error);
process.exit(1);
});
@@ -1,197 +0,0 @@
import path from 'node:path';
import { spawn, spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const DESKTOP_DEV_PORT = 3901;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '..', '..', '..');
const desktopDir = path.join(repoRoot, 'packages', 'desktop');
const tauriDir = path.join(desktopDir, 'src-tauri');
const inferTargetTriple = () => {
const fromEnv = typeof process.env.TAURI_ENV_TARGET_TRIPLE === 'string' ? process.env.TAURI_ENV_TARGET_TRIPLE.trim() : '';
if (fromEnv) return fromEnv;
if (process.platform === 'darwin') {
return process.arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin';
}
if (process.platform === 'win32') {
return 'x86_64-pc-windows-msvc';
}
if (process.platform === 'linux') {
return process.arch === 'arm64' ? 'aarch64-unknown-linux-gnu' : 'x86_64-unknown-linux-gnu';
}
return `${process.arch}-${process.platform}`;
};
const targetTriple = inferTargetTriple();
const sidecarName = process.platform === 'win32'
? `openchamber-server-${targetTriple}.exe`
: `openchamber-server-${targetTriple}`;
const sidecarPath = path.join(tauriDir, 'sidecars', sidecarName);
const distDir = path.join(tauriDir, 'resources', 'web-dist');
const webDir = path.join(repoRoot, 'packages', 'web');
const run = (cmd, args, cwd) => {
const result = spawnSync(cmd, args, { cwd, stdio: 'inherit' });
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`Command failed: ${cmd} ${args.join(' ')}`);
}
};
console.log('[desktop] ensuring sidecar + web-dist...');
run('node', ['./scripts/build-sidecar.mjs'], desktopDir);
console.log(`[desktop] starting API server on http://127.0.0.1:${DESKTOP_DEV_PORT} ...`);
const apiChild = spawn(sidecarPath, ['--port', String(DESKTOP_DEV_PORT)], {
cwd: repoRoot,
stdio: 'inherit',
detached: process.platform !== 'win32',
env: {
...process.env,
OPENCHAMBER_HOST: '127.0.0.1',
OPENCHAMBER_DIST_DIR: distDir,
NO_PROXY: process.env.NO_PROXY || 'localhost,127.0.0.1',
no_proxy: process.env.no_proxy || 'localhost,127.0.0.1',
},
});
console.log('[desktop] starting Vite HMR server on http://127.0.0.1:5173 ...');
const webChild = spawn('bun', ['x', 'vite', '--host', '127.0.0.1', '--port', '5173', '--strictPort'], {
cwd: webDir,
stdio: 'inherit',
detached: process.platform !== 'win32',
env: {
...process.env,
OPENCHAMBER_PORT: String(DESKTOP_DEV_PORT),
NO_PROXY: process.env.NO_PROXY || 'localhost,127.0.0.1',
no_proxy: process.env.no_proxy || 'localhost,127.0.0.1',
},
});
let shuttingDown = false;
function waitForExit(child, timeoutMs) {
return new Promise((resolve) => {
if (!child || child.exitCode !== null || child.signalCode !== null) {
resolve();
return;
}
const onExit = () => {
clearTimeout(timer);
resolve();
};
const timer = setTimeout(() => {
child.off('exit', onExit);
resolve();
}, timeoutMs);
child.once('exit', onExit);
});
}
function signalChild(child, signal) {
if (!child || child.exitCode !== null || child.signalCode !== null) {
return;
}
try {
if (process.platform !== 'win32') {
process.kill(-child.pid, signal);
return;
}
} catch {
}
try {
child.kill(signal);
} catch {
}
}
async function requestApiShutdown() {
const url = `http://127.0.0.1:${DESKTOP_DEV_PORT}/api/system/shutdown`;
try {
await fetch(url, { method: 'POST' });
} catch {
}
}
async function stopChildTree(child) {
if (!child || child.exitCode !== null || child.signalCode !== null) {
return;
}
signalChild(child, 'SIGINT');
await waitForExit(child, 2500);
if (child.exitCode === null && child.signalCode === null) {
signalChild(child, 'SIGTERM');
await waitForExit(child, 2500);
}
if (child.exitCode === null && child.signalCode === null) {
signalChild(child, 'SIGKILL');
await waitForExit(child, 1000);
}
}
const shutdown = async (exitCode = 0) => {
if (shuttingDown) return;
shuttingDown = true;
await requestApiShutdown();
await Promise.all([stopChildTree(webChild), stopChildTree(apiChild)]);
process.exit(exitCode);
};
const handleExit = (label) => (code, signal) => {
if (shuttingDown) {
return;
}
if (code !== 0 || signal) {
console.error(`[desktop] ${label} exited unexpectedly (code=${code ?? 'null'} signal=${signal ?? 'none'})`);
}
shutdown(typeof code === 'number' ? code : 1).catch((error) => {
console.error('[desktop] shutdown failed:', error);
process.exit(1);
});
};
apiChild.on('exit', handleExit('API server'));
webChild.on('exit', handleExit('Vite server'));
const handleError = (label) => (error) => {
if (shuttingDown) {
return;
}
console.error(`[desktop] failed to start ${label}:`, error);
shutdown(1).catch(() => process.exit(1));
};
apiChild.on('error', handleError('API server'));
webChild.on('error', handleError('Vite server'));
process.on('SIGINT', () => {
shutdown(130).catch(() => process.exit(130));
});
process.on('SIGTERM', () => {
shutdown(143).catch(() => process.exit(143));
});
process.on('SIGHUP', () => {
shutdown(129).catch(() => process.exit(129));
});
@@ -1,237 +0,0 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { access, readFile, unlink, writeFile } from 'node:fs/promises';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const desktopDir = path.resolve(__dirname, '..');
const stateFile = path.join(desktopDir, '.opencode-cli-state.json');
const DEFAULT_BIN_CANDIDATES = [
process.env.OPENCHAMBER_OPENCODE_PATH,
process.env.OPENCHAMBER_OPENCODE_BIN,
process.env.OPENCODE_PATH,
process.env.OPENCODE_BINARY,
'/opt/homebrew/bin/opencode',
'/usr/local/bin/opencode',
'/usr/bin/opencode',
path.join(os.homedir(), '.local/bin/opencode'),
].filter(Boolean);
const CLI_ARGS_ENV = process.env.OPENCHAMBER_OPENCODE_ARGS;
const DEFAULT_ARGS = CLI_ARGS_ENV
? parseArgs(CLI_ARGS_ENV)
: ['api'];
function parseArgs(raw) {
if (!raw || typeof raw !== 'string') {
return [];
}
const trimmed = raw.trim();
if (!trimmed) {
return [];
}
if (trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed);
if (Array.isArray(parsed) && parsed.every((item) => typeof item === 'string')) {
return parsed;
}
} catch {
// fall through to whitespace split
}
}
return trimmed.split(/\s+/g);
}
async function fileExists(targetPath) {
try {
await access(targetPath, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
async function resolveCliPath() {
for (const candidate of DEFAULT_BIN_CANDIDATES) {
if (candidate && await fileExists(candidate)) {
return candidate;
}
}
const envPath = process.env.PATH || '';
for (const segment of envPath.split(path.delimiter)) {
const candidate = path.join(segment, 'opencode');
if (await fileExists(candidate)) {
return candidate;
}
}
throw new Error('Unable to locate the OpenCode CLI. Set OPENCHAMBER_OPENCODE_PATH to the executable.');
}
async function readState() {
try {
const raw = await readFile(stateFile, 'utf8');
const data = JSON.parse(raw);
if (typeof data?.pid === 'number') {
return data;
}
} catch {
// ignore
}
return null;
}
function isProcessAlive(pid) {
if (!pid || typeof pid !== 'number') {
return false;
}
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
async function writeState(pid) {
await writeFile(stateFile, JSON.stringify({ pid }), 'utf8');
}
async function removeStateFile() {
try {
await unlink(stateFile);
} catch {
// already removed
}
}
function spawnCli(cliPath, args) {
const env = {
...process.env,
OPENCHAMBER_OPENCODE_PORT: process.env.OPENCHAMBER_OPENCODE_PORT || process.env.OPENCODE_PORT || process.env.OPENCHAMBER_INTERNAL_PORT || '0',
};
const cwd = process.env.OPENCHAMBER_OPENCODE_CWD || process.cwd();
const child = spawn(cliPath, args.length > 0 ? args : DEFAULT_ARGS, {
cwd,
env,
detached: true,
stdio: 'ignore',
});
child.unref();
return child;
}
export async function startCli({ silent = false } = {}) {
const existing = await readState();
if (existing?.pid && isProcessAlive(existing.pid)) {
if (!silent) {
console.log(`[desktop:start-cli] OpenCode CLI already running (pid ${existing.pid}).`);
}
return existing.pid;
}
const cliPath = await resolveCliPath();
const child = spawnCli(cliPath, DEFAULT_ARGS);
await writeState(child.pid);
if (!silent) {
console.log(`[desktop:start-cli] OpenCode CLI started (${cliPath}) pid ${child.pid}.`);
}
return child.pid;
}
export async function stopCli({ silent = false } = {}) {
const state = await readState();
if (!state?.pid) {
if (!silent) {
console.log('[desktop:stop-cli] No OpenCode CLI PID recorded.');
}
return;
}
const { pid } = state;
if (!isProcessAlive(pid)) {
await removeStateFile();
if (!silent) {
console.log('[desktop:stop-cli] CLI already stopped.');
}
return;
}
try {
process.kill(pid, 'SIGTERM');
} catch (error) {
if (!silent) {
console.error(`[desktop:stop-cli] Failed to send SIGTERM to pid ${pid}:`, error);
}
}
const timeoutMs = 5000;
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (!isProcessAlive(pid)) {
await removeStateFile();
if (!silent) {
console.log('[desktop:stop-cli] OpenCode CLI stopped.');
}
return;
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
try {
process.kill(pid, 'SIGKILL');
if (!silent) {
console.warn(`[desktop:stop-cli] Forced termination sent to pid ${pid}.`);
}
} catch (error) {
if (!silent) {
console.error(`[desktop:stop-cli] Unable to terminate pid ${pid}:`, error);
}
} finally {
await removeStateFile();
}
}
async function main() {
const [, , command] = process.argv;
if (!command || command === '--help' || command === '-h') {
console.log('Usage: node opencode-cli.mjs <start|stop|status>');
process.exit(0);
}
if (command === 'start') {
await startCli();
return;
}
if (command === 'stop') {
await stopCli();
return;
}
if (command === 'status') {
const state = await readState();
if (state?.pid && isProcessAlive(state.pid)) {
console.log(`OpenCode CLI running (pid ${state.pid}).`);
} else {
console.log('OpenCode CLI not running.');
}
process.exit(0);
return;
}
console.error(`Unknown command: ${command}`);
process.exit(1);
}
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
main().catch((error) => {
console.error('[desktop:opencode-cli] Unexpected error:', error);
process.exit(1);
});
}
File diff suppressed because it is too large Load Diff
-42
View File
@@ -1,42 +0,0 @@
[package]
name = "openchamber-desktop"
version = "1.9.9"
edition = "2021"
publish = false
[[bin]]
name = "openchamber-desktop"
path = "src/main.rs"
[features]
default = []
devtools = ["tauri/devtools"]
[dependencies]
anyhow = "1.0.86"
base64 = "0.22.1"
log = "0.4.28"
reqwest = { version = "0.12.4", default-features = false, features = ["rustls-tls", "json"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.143"
tauri = { version = "2.10.3", features = ["macos-private-api"] }
tauri-plugin-dialog = "2.6.0"
tauri-plugin-log = "2.8.0"
tauri-plugin-shell = "2.3.5"
tauri-plugin-notification = "2.3.3"
tauri-plugin-updater = "2.10.0"
tokio = { version = "1.38", features = ["rt-multi-thread", "time", "macros", "sync"] }
url = "2.5"
[build-dependencies]
tauri-build = { version = "2.5.6", features = [] }
[profile.release]
lto = "thin"
codegen-units = 1
strip = true
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-web-kit = "0.3"
rfd = "0.15"
-27
View File
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSSupportsAutomaticTermination</key>
<false/>
<key>NSSupportsSuddenTermination</key>
<false/>
<key>NSAppleEventsUsageDescription</key>
<string>OpenChamber needs to run the OpenCode CLI to provide AI coding assistance.</string>
<key>NSDesktopFolderUsageDescription</key>
<string>OpenChamber needs access to work with your projects.</string>
<key>NSDocumentsFolderUsageDescription</key>
<string>OpenChamber needs access to work with your projects.</string>
<key>NSDownloadsFolderUsageDescription</key>
<string>OpenChamber needs access to work with your projects.</string>
<key>NSMicrophoneUsageDescription</key>
<string>OpenChamber needs microphone access for voice input.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>OpenChamber needs speech recognition to transcribe voice input.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
</dict>
</plist>
-3
View File
@@ -1,3 +0,0 @@
fn main() {
tauri_build::build();
}
@@ -1,41 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capabilities for OpenChamber desktop runtime",
"remote": {
"urls": [
"http://127.0.0.1:*/*",
"http://localhost:*/*",
"http://*",
"http://*/*",
"https://*",
"https://*/*"
]
},
"windows": ["main", "main-*"],
"permissions": [
"core:default",
"core:window:default",
"core:window:allow-close",
"core:window:allow-set-title",
"core:window:allow-set-size",
"core:window:allow-set-position",
"core:window:allow-start-dragging",
"core:webview:default",
"core:webview:allow-webview-close",
"shell:allow-open",
"shell:allow-execute",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
"notification:default",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:allow-notify",
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install"
]
}
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!--
Intentionally NOT sandboxed. This app is distributed outside the Mac App Store.
Do not add com.apple.security.app-sandbox.
These entitlements are commonly required for WKWebView/WebKit JIT behavior
under hardened runtime.
-->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="iconShadow" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="12" stdDeviation="14" flood-opacity="0.5" flood-color="#000000"/>
</filter>
<linearGradient id="bgGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#303030"/>
<stop offset="100%" stop-color="#141414"/>
</linearGradient>
</defs>
<!-- Icon with Apple standard padding (100px on each side) -->
<g transform="translate(100, 100)">
<!-- Background rounded square - 824x824 (Apple standard) - dark gradient -->
<rect x="0" y="0" width="824" height="824" rx="185" ry="185" fill="url(#bgGradient)" filter="url(#iconShadow)"/>
<!-- OpenChamber logo centered - simplified for dock visibility -->
<g transform="translate(412, 412) scale(6.5)">
<!-- Left face - simplified, no grid cells -->
<path d="M0 0 L-41.568 -24 L-41.568 24 L0 48 Z" fill="white" fill-opacity="0.2" stroke="white" stroke-width="3" stroke-linejoin="round"/>
<!-- Right face - simplified, no grid cells -->
<path d="M0 0 L41.568 -24 L41.568 24 L0 48 Z" fill="white" fill-opacity="0.35" stroke="white" stroke-width="3" stroke-linejoin="round"/>
<!-- Top face - open -->
<path d="M0 -48 L-41.568 -24 L0 0 L41.568 -24 Z" fill="none" stroke="white" stroke-width="3" stroke-linejoin="round"/>
<!-- OpenCode logo on top face -->
<g transform="matrix(0.866, 0.5, -0.866, 0.5, 0, -24) scale(0.75)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-16 -20 L16 -20 L16 20 L-16 20 Z M-8 -12 L-8 12 L8 12 L8 -12 Z" fill="white"/>
<path d="M-8 -4 L8 -4 L8 12 L-8 12 Z" fill="white" fill-opacity="0.4"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,82 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"productName": "OpenChamber",
"version": "1.9.9",
"identifier": "ai.opencode.openchamber",
"build": {
"beforeDevCommand": "node ./scripts/dev-web-server.mjs",
"beforeBuildCommand": "bun run build:sidecar",
"devUrl": "http://127.0.0.1:3901",
"frontendDist": "../noop-dist"
},
"app": {
"windows": [
{
"label": "main",
"create": false,
"title": "OpenChamber",
"width": 1280,
"height": 800,
"resizable": true,
"fullscreen": false,
"decorations": true,
"hiddenTitle": true,
"titleBarStyle": "Overlay",
"trafficLightPosition": {
"x": 17,
"y": 26
},
"dragDropEnabled": false,
"visible": false
}
],
"security": {
"csp": null
},
"withGlobalTauri": true,
"macOSPrivateApi": true
},
"bundle": {
"active": true,
"externalBin": [
"sidecars/openchamber-server"
],
"resources": [
"resources/web-dist/**/*"
],
"icon": [
"icons/icon.icns",
"icons/icon.png"
],
"macOS": {
"exceptionDomain": "localhost",
"minimumSystemVersion": "13.0",
"signingIdentity": null,
"entitlements": "./entitlements.plist",
"infoPlist": "Info.plist",
"dmg": {
"appPosition": {
"x": 180,
"y": 170
},
"applicationFolderPosition": {
"x": 480,
"y": 170
},
"windowSize": {
"width": 660,
"height": 400
}
}
},
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"endpoints": [
"https://github.com/btriapitsyn/openchamber/releases/latest/download/latest.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwRDMyQUQzNjNFRTc1ODIKUldTQ2RlNWoweXJUSUdpVWVWNm84R1pHamYzNVFhYWgyWmlpWFVzem5nUTlHd1dlRlNTV0FFc3IK"
}
}
}
@@ -1,9 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"bundle": {
"icon": [
"icons/dev-icon.icns",
"icons/dev-icon.png"
]
}
}
-57
View File
@@ -1,57 +0,0 @@
# Docs Authoring Guide
This package is docs content source-of-truth for OpenChamber.
## Add a new docs page
1. Create a new file in `packages/docs/content/docs/`.
- Example: `packages/docs/content/docs/remote-access.mdx`
2. Add frontmatter at top:
```mdx
---
title: Remote Access
description: Access OpenChamber from outside your local network.
---
```
3. Use route-safe naming:
- `foo.mdx` -> `/foo/`
- `folder/index.mdx` -> `/folder/`
- `folder/bar.mdx` -> `/folder/bar/`
4. Run validation:
```bash
bun run docs:validate
```
## Add a new sidebar section
Edit `packages/docs/sidebar.config.json`.
Example:
```json
{
"label": "Advanced",
"items": [{ "label": "Remote Access", "link": "/remote-access/" }]
}
```
Rules:
- use trailing slash in links (`/page/`)
- every sidebar link must map to an existing MDX file
- keep section labels short and task-oriented
## Sync into openchamber-website
`openchamber-website` renders/deploys docs via Starlight in `apps/docs`.
After docs content updates here:
1. copy `packages/docs/content/docs/*` -> `openchamber-website/apps/docs/src/content/docs/*`
2. map `packages/docs/sidebar.config.json` into `openchamber-website/apps/docs/astro.config.mjs` sidebar
3. run docs checks/build in website repo
Automation support exists in `.github/workflows/docs-source.yml` (release/manual packaging of docs source artifact).
-42
View File
@@ -1,42 +0,0 @@
# Docs Source Deployment
This repo publishes docs **source artifacts**.
Rendering and hosting still happen in `openchamber-website` (`apps/docs`).
## Workflow
Use `.github/workflows/docs-source.yml`.
Triggers:
- push to `main` when docs source changes
- release published
- manual `workflow_dispatch`
Outputs:
- validates docs (`bun run docs:validate`)
- creates `openchamber-docs-source-<sha>.tar.gz`
- uploads archive as workflow artifact
- on release/manual with tag, uploads archive to release assets
## Optional cross-repo sync trigger
The workflow can trigger a `repository_dispatch` event in `openchamber-website`.
Set secret in this repo:
- `OPENCHAMBER_WEBSITE_REPO_TOKEN` (token with access to `openchamber/openchamber-website`)
Event sent:
- `event_type: docs_source_updated`
Payload includes:
- `source_repo`
- `source_ref`
- `archive_name`
`openchamber-website` can listen for this event and pull docs source from release artifacts.
-31
View File
@@ -1,31 +0,0 @@
# OpenChamber Docs Source
This package is the source-of-truth for OpenChamber public docs content.
## Layout
- `content/docs/*.mdx` - English docs pages
- `sidebar.config.json` - docs navigation structure for Starlight sidebar
- `CONTRIBUTING.md` - authoring guide for adding pages and sections
- `DEPLOYMENT.md` - release/manual packaging and sync trigger model
## Local validation
Run from repo root:
```bash
bun run docs:validate
```
This validates:
- frontmatter (`title`, `description`) exists for every MDX page
- sidebar links resolve to existing MDX routes
## Deployment model
This repo owns docs content.
Website rendering/deployment happens in `openchamber-website` (`apps/docs`).
Use `.github/workflows/docs-source.yml` to package docs source on release or manual trigger.
-25
View File
@@ -1,25 +0,0 @@
---
title: OpenChamber Docs
description: Setup and operating guide for OpenChamber across web, desktop, and VS Code.
---
# OpenChamber Docs
OpenChamber is the visual workspace around OpenCode.
Use these docs to:
- install the right surface for your workflow
- expose OpenChamber safely for remote use
- customize appearance and troubleshoot common issues
## Read this first
- [Install](/install/)
- [Quickstart](/quickstart/)
- [Tunnels](/tunnels/)
- [Troubleshooting](/troubleshooting/)
## What OpenChamber is for
OpenChamber is for the parts of AI coding that benefit from a control room: branching sessions, reviewing diffs, managing terminals, watching tool progress, running project actions, and keeping the full board visible while the agent works.
@@ -1,33 +0,0 @@
---
title: Install
description: Install OpenChamber for desktop, web, or VS Code.
---
# Install
OpenChamber has three main surfaces:
- desktop app for macOS
- CLI-hosted web app with installable PWA
- VS Code extension
## Prerequisite
Install [OpenCode](https://opencode.ai) first.
## Web + PWA
```bash
curl -fsSL https://raw.githubusercontent.com/openchamber/openchamber/main/scripts/install.sh | bash
openchamber --ui-password be-creative-here
```
Then open the URL printed by the CLI (usually `http://localhost:3000`).
## Desktop
Download the latest desktop build from the GitHub releases page or the OpenChamber download page.
## VS Code
Install from the VS Code Marketplace and sign into your usual OpenCode workflow.
@@ -1,22 +0,0 @@
---
title: Quickstart
description: Start OpenChamber quickly and choose the right surface for the task.
---
# Quickstart
## Fastest path
1. Install OpenCode.
2. Install OpenChamber CLI.
3. Run `openchamber --ui-password be-creative-here`.
4. Open the web UI on your machine.
5. If needed, start a tunnel and scan the QR code from your phone.
Use a strong UI password, especially if you plan to expose the instance remotely.
## Which surface should I use?
- use **desktop** for macOS-heavy daily driver workflows
- use **web** for remote access and mobile review
- use **VS Code** for editor-native sessions beside code
@@ -1,347 +0,0 @@
---
title: Reverse Proxy
description: Configure OpenChamber correctly behind Nginx, Nginx Proxy Manager, or another reverse proxy.
---
# Reverse Proxy
Use this page if you run OpenChamber behind Nginx, Nginx Proxy Manager, Caddy, Cloudflare, or another reverse proxy.
## Before you proxy it
1. Confirm OpenChamber works directly first.
2. Open `http://<server-ip>:3000` or your custom port from the same network.
3. Only add the reverse proxy after the direct connection works.
## What the proxy must support
- WebSockets for live message transport:
- `/api/event/ws`
- `/api/global/event/ws`
- `/api/terminal/ws`
- SSE without buffering:
- `/api/event`
- `/api/global/event`
- `/api/notifications/stream`
- `/api/openchamber/events`
- `/api/terminal/:sessionId/stream`
- Large request bodies for attachments and file operations
- Long-lived read timeouts for live streams and terminal sessions
## Rules that matter
- Enable WebSocket proxying.
- Disable buffering on SSE routes.
- Disable gzip on the proxy if OpenChamber is already compressing responses.
- Keep compression enabled in only one layer.
- Forward normal proxy headers such as `Host`, `X-Forwarded-For`, and `X-Forwarded-Proto`.
- Increase body size limits if users upload files.
## Quick checklist
- OpenChamber reachable directly on LAN
- WebSockets enabled in the proxy
- SSE routes have buffering off
- `gzip off` on the proxy host, or proxy compression disabled another way
- `client_max_body_size` large enough for attachments
- `proxy_read_timeout` long enough for streams
## Example: Nginx
<details>
<summary>Show example config</summary>
```nginx
client_max_body_size 50M;
client_body_buffer_size 50M;
proxy_request_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
gzip off;
location = /api/terminal/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location = /api/global/event/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location = /api/event/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location ~ ^/api/(event|global/event|notifications/stream|openchamber/events)$ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location ~ ^/api/terminal/.+/stream$ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location /api {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
```
</details>
## Example: Nginx Proxy Manager
<details>
<summary>Show Advanced tab example</summary>
```nginx
client_max_body_size 50M;
client_body_buffer_size 50M;
proxy_request_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
gzip off;
location = /api/terminal/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/global/event/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/event/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/event {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/global/event {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/notifications/stream {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/openchamber/events {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location ~ ^/api/terminal/.+/stream$ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location /api {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
```
</details>
Also enable `Websockets Support` in Nginx Proxy Manager for this host.
## Common failure signs
### Page loads, but sending messages fails
- WebSockets are not enabled in the proxy
- `/api/event/ws` or `/api/global/event/ws` is not passing through correctly
### Notifications or live status do not update
- one of the SSE routes is buffered or cached
- `X-Accel-Buffering "no"` is missing
### File uploads fail
- `client_max_body_size` is too small
### Everything works locally, but breaks only behind the proxy
- the proxy is compressing and buffering live traffic
- the proxy is missing WebSocket support
## Example: Caddy
<details>
<summary>Show example config</summary>
```caddy
reverse_proxy 127.0.0.1:3000 {
# WebSocket support is automatic in Caddy
# Flush SSE responses immediately
flush_interval -1
# Pass through Host and proxy headers
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
# Increase timeouts for long-lived streams
transport http {
read_timeout 3600s
write_timeout 3600s
}
}
```
</details>
Caddy handles WebSocket upgrades automatically — no extra configuration needed. The `flush_interval -1` directive ensures SSE chunks are forwarded immediately without buffering.
## CDN and double-compression warning
If you place a CDN (such as Cloudflare) in front of your reverse proxy, be aware of double compression:
- OpenChamber compresses HTTP responses with gzip (threshold 1 KB).
- Cloudflare and other CDNs also compress responses by default.
- This can cause double-compressed responses or incorrect `Content-Encoding` headers.
To avoid this, disable compression at **one** layer:
- **Cloudflare:** Rules → Compression → disable (or use "Passthrough" mode).
- **Nginx:** `gzip off` (already shown in the examples above).
- **Caddy:** Caddy does not re-compress by default if the upstream already sends compressed content.
SSE streaming routes are excluded from compression by OpenChamber, but the CDN may still buffer them. Check your CDN documentation for how to disable buffering on SSE paths.
## Related
- [Tunnels](/tunnels/)
- [Troubleshooting](/troubleshooting/)
-30
View File
@@ -1,30 +0,0 @@
---
title: Themes
description: Customize OpenChamber with built-in and user-defined themes.
---
# Themes
OpenChamber supports built-in themes and custom theme JSON files.
## Add a custom theme
1. Create the themes directory:
```bash
mkdir -p ~/.config/openchamber/themes
```
2. Add your JSON file to that directory (for example `my-theme.json`).
3. Open OpenChamber, then go to **Settings -> Theme -> Reload themes**.
4. Pick your theme from the dropdown.
## Theme location
- macOS/Linux: `~/.config/openchamber/themes/`
## Full JSON format reference
Use the full format guide in the main repo docs:
- [`docs/CUSTOM_THEMES.md`](https://github.com/openchamber/openchamber/blob/main/docs/CUSTOM_THEMES.md)
@@ -1,30 +0,0 @@
---
title: Troubleshooting
description: Common setup and runtime issues with quick fixes.
---
# Troubleshooting
## OpenChamber command exits or fails to start
- confirm Node.js `>=20`
- run `openchamber --version`
- reinstall latest CLI if needed
## Web UI is not reachable
- check server logs with `openchamber logs`
- verify active port (default `3000`)
- open `http://localhost:3000` directly first before testing tunnel links
## Remote/tunnel link does not work
- run `openchamber tunnel status --all`
- restart tunnel from the same instance/port
- regenerate connect link if previous token was already used
## VS Code extension does not connect
- confirm OpenChamber server is running
- verify extension is updated
- reload VS Code window and retry connection
@@ -1,77 +0,0 @@
---
title: Tunnels
description: Expose OpenChamber safely for remote and mobile access.
---
# Tunnels
Use `openchamber tunnel` to expose a running OpenChamber instance.
## Quick start (Cloudflare quick mode)
1. Start OpenChamber:
```bash
openchamber
```
2. Start a tunnel:
```bash
openchamber tunnel start --provider cloudflare --mode quick
```
3. Check status:
```bash
openchamber tunnel status
```
By default, OpenChamber prints a QR code in interactive TTY sessions. Use `--qr` to force QR output, or `--no-qr` to disable it.
## Managed modes
### Managed remote
Use a token + hostname managed by Cloudflare:
```bash
openchamber tunnel start --provider cloudflare --mode managed-remote --token-file ~/.secrets/cf-token --hostname app.example.com
```
### Managed local
Use a local `cloudflared` config:
```bash
openchamber tunnel start --provider cloudflare --mode managed-local --config ~/.cloudflared/config.yml
```
## Profiles (managed-remote)
Save a reusable profile:
```bash
openchamber tunnel profile add --provider cloudflare --mode managed-remote --name prod-main --hostname app.example.com --token-file ~/.secrets/cf-token
```
Start using the saved profile:
```bash
openchamber tunnel start --profile prod-main
```
## Useful commands
```bash
openchamber tunnel providers
openchamber tunnel ready --provider cloudflare
openchamber tunnel doctor --provider cloudflare
openchamber tunnel stop --port 3000
```
## Behavior notes
- one active tunnel per OpenChamber instance (port)
- starting a new mode/provider on same instance replaces previous tunnel
- generating a new connect link revokes previous unused one
-26
View File
@@ -1,26 +0,0 @@
{
"sections": [
{
"label": "Start here",
"items": [
{ "label": "Overview", "link": "/" },
{ "label": "Install", "link": "/install/" },
{ "label": "Quickstart", "link": "/quickstart/" },
{ "label": "Tunnels", "link": "/tunnels/" }
]
},
{
"label": "Customize",
"items": [
{ "label": "Themes", "link": "/themes/" }
]
},
{
"label": "Help",
"items": [
{ "label": "Reverse Proxy", "link": "/reverse-proxy/" },
{ "label": "Troubleshooting", "link": "/troubleshooting/" }
]
}
]
}
-13
View File
@@ -1,13 +0,0 @@
# Dependencies
node_modules/
# Electron build output
dist/
dist-bundle/
# Generated packaging resources
resources/web-dist/
resources/sidecar/
# OS-specific
.DS_Store
File diff suppressed because it is too large Load Diff
-98
View File
@@ -1,98 +0,0 @@
{
"name": "@openchamber/electron",
"version": "1.9.9",
"private": true,
"description": "Electron desktop runtime for OpenChamber",
"author": "OpenChamber",
"type": "module",
"main": "./dist-bundle/main.mjs",
"dependencies": {
"@openchamber/web": "workspace:*",
"electron-context-menu": "^4.1.2",
"electron-log": "^5.4.3",
"electron-updater": "^6.8.3"
},
"devDependencies": {
"@electron/rebuild": "^3.7.0",
"electron": "^41.2.1",
"electron-builder": "^26.0.0"
},
"desktopPrerequisites": [
"Electron runtime dependencies installed via bun install",
"Bun available for sidecar compilation",
"macOS build tools installed for notarized packaging"
],
"scripts": {
"dev": "node ./scripts/electron-dev.mjs",
"build:web-assets": "node ./scripts/build-web-assets.mjs",
"build": "bun -e \"process.exit(0)\"",
"bundle:main": "bun ./scripts/bundle-main.mjs",
"rebuild:native": "node ./scripts/rebuild-native.mjs",
"package": "bun run build:web-assets && bun run bundle:main && bun run rebuild:native && electron-builder",
"finalize:latest-yml": "node ./scripts/finalize-latest-yml.mjs",
"type-check": "node --check ./main.mjs && node --check ./preload.mjs",
"lint": "node -e \"process.exit(0)\""
},
"build": {
"appId": "dev.openchamber.desktop",
"productName": "OpenChamber",
"files": [
"dist-bundle/main.mjs",
"preload.mjs"
],
"extraResources": [
{
"from": "resources/web-dist",
"to": "web-dist"
}
],
"directories": {
"buildResources": "resources/icons",
"output": "dist"
},
"artifactName": "${productName}-${version}-${arch}.${ext}",
"npmRebuild": false,
"mac": {
"category": "public.app-category.developer-tools",
"icon": "resources/icons/icon.icns",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "resources/entitlements.mac.plist",
"entitlementsInherit": "resources/entitlements.mac.plist",
"notarize": true,
"target": [
"dmg",
"zip"
]
},
"dmg": {
"sign": true,
"title": "${productName} ${version}",
"backgroundColor": "#FFFCF0",
"iconSize": 100,
"iconTextSize": 13,
"window": {
"width": 540,
"height": 340
},
"contents": [
{
"x": 180,
"y": 140,
"type": "file"
},
{
"x": 360,
"y": 140,
"type": "link",
"path": "/Applications"
}
]
},
"publish": {
"provider": "github",
"owner": "btriapitsyn",
"repo": "openchamber"
}
}
}
-146
View File
@@ -1,146 +0,0 @@
import { contextBridge, ipcRenderer } from 'electron';
const eventListeners = new Map();
const readArgValue = (name) => {
const prefix = `${name}=`;
const entry = process.argv.find((value) => typeof value === 'string' && value.startsWith(prefix));
if (!entry) {
return '';
}
return entry.slice(prefix.length);
};
const localOrigin = readArgValue('--openchamber-local-origin');
const homeDirectory = readArgValue('--openchamber-home');
const macosMajorRaw = readArgValue('--openchamber-macos-major');
const macosMajor = Number.parseInt(macosMajorRaw, 10);
// Preload re-executes on every cross-origin navigation (we run with
// sandbox:false, per-document). Two separate concerns to balance:
// - __OPENCHAMBER_ELECTRON__ is a shell-identity flag (no capability).
// Remote UIs still need it so isDesktopShell() returns true and the
// window renders with desktop affordances (DesktopHostSwitcher,
// title bar offsets, etc.). Expose unconditionally.
// - __TAURI__ is the IPC channel to the main process. Remote pages must
// not get it — otherwise any page loaded via DesktopHostSwitcher could
// read local files, open apps, relaunch, etc. Expose only on local
// pages (loopback / state.localOrigin / file:// for dev).
// Everything driven by localOrigin (home dir, macOS hints) also stays
// local-only since it leaks info about the Electron host machine.
const currentOrigin = (() => {
try {
return typeof location !== 'undefined' ? location.origin : '';
} catch {
return '';
}
})();
const isLoopbackOrigin = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i.test(currentOrigin);
const isLocalPage = currentOrigin === 'null'
|| isLoopbackOrigin
|| (localOrigin && currentOrigin === localOrigin);
// Remote pages need __OPENCHAMBER_LOCAL_ORIGIN__ so the HostSwitcher knows
// the URL of the Local entry (isDesktopLocalOriginActive() falls back to
// window.location.origin otherwise — wrong on remote). Low risk: the value
// is just "http://127.0.0.1:<port>" which is not exploitable without the
// IPC channel, and CORS on the local server prevents remote-origin fetches.
if (localOrigin) {
contextBridge.exposeInMainWorld('__OPENCHAMBER_LOCAL_ORIGIN__', localOrigin);
}
// Home directory leaks the OS username — keep local-only. Remote pages
// operate on the REMOTE server's filesystem, local home is irrelevant
// (and would be misleading if consumed as a workspace hint).
if (isLocalPage && homeDirectory) {
contextBridge.exposeInMainWorld('__OPENCHAMBER_HOME__', homeDirectory);
}
// macOS major version drives window chrome offsets (traffic lights) — UI
// presentation only, safe to expose.
if (Number.isFinite(macosMajor) && macosMajor > 0) {
contextBridge.exposeInMainWorld('__OPENCHAMBER_MACOS_MAJOR__', macosMajor);
}
contextBridge.exposeInMainWorld('__OPENCHAMBER_ELECTRON__', {
runtime: 'electron',
});
// Note: bootOutcome must stay writable from the main world's initScript so
// re-navigations (host switch via deep link) can refresh it. contextBridge-
// exposed globals are read-only, which blocks that update — rely solely on
// the main-process initScript injection (dispatched on did-finish-load).
const addListener = (event, handler) => {
const listeners = eventListeners.get(event) || new Set();
listeners.add(handler);
eventListeners.set(event, listeners);
return () => {
const current = eventListeners.get(event);
if (!current) {
return;
}
current.delete(handler);
if (current.size === 0) {
eventListeners.delete(event);
}
};
};
const dispatchNativeEvent = (event, detail) => {
const listeners = eventListeners.get(event);
if (listeners) {
for (const listener of listeners) {
try {
listener({ payload: detail });
} catch (error) {
console.error(`[electron:preload] listener failed for ${event}:`, error);
}
}
}
try {
const domEvent = detail === undefined
? new Event(event)
: new CustomEvent(event, { detail });
window.dispatchEvent(domEvent);
} catch (error) {
console.error(`[electron:preload] failed to dispatch DOM event ${event}:`, error);
}
};
// Main-process events are read-only notifications (update progress,
// window focus, etc.) — safe to deliver to any page rendered in this
// webContents. The events themselves don't grant capability.
ipcRenderer.on('openchamber:emit', (_evt, payload) => {
if (!payload || typeof payload !== 'object') {
return;
}
const event = typeof payload.event === 'string' ? payload.event : '';
if (!event) {
return;
}
dispatchNativeEvent(event, payload.detail);
});
// __TAURI__ is exposed on all pages; the main-process gate in
// ipcMain.handle('openchamber:invoke') decides per-command what is safe
// for non-local callers (window/host-switcher ops yes, file/shell ops
// no). See COMMANDS_SAFE_FOR_REMOTE in main.mjs.
contextBridge.exposeInMainWorld('__TAURI__', {
core: {
invoke: (cmd, args) => ipcRenderer.invoke('openchamber:invoke', cmd, args || {}),
},
dialog: {
open: (options) => ipcRenderer.invoke('openchamber:dialog:open', options || {}),
},
shell: {
open: (url) => ipcRenderer.invoke('openchamber:invoke', 'desktop_open_external_url', { url }),
},
event: {
listen: async (event, handler) => addListener(event, handler),
},
});
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!--
Intentionally NOT sandboxed. This app is distributed outside the Mac App Store.
Do not add com.apple.security.app-sandbox.
These entitlements are commonly required for Electron/Chromium JIT behavior
under hardened runtime, plus access the app already relies on (microphone for
voice notes / dictation, network client for sidecar + remote hosts, shell
probing for PATH inheritance).
-->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="iconShadow" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="12" stdDeviation="14" flood-opacity="0.5" flood-color="#000000"/>
</filter>
<linearGradient id="bgGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#303030"/>
<stop offset="100%" stop-color="#141414"/>
</linearGradient>
</defs>
<!-- Icon with Apple standard padding (100px on each side) -->
<g transform="translate(100, 100)">
<!-- Background rounded square - 824x824 (Apple standard) - dark gradient -->
<rect x="0" y="0" width="824" height="824" rx="185" ry="185" fill="url(#bgGradient)" filter="url(#iconShadow)"/>
<!-- OpenChamber logo centered - simplified for dock visibility -->
<g transform="translate(412, 412) scale(6.5)">
<!-- Left face - simplified, no grid cells -->
<path d="M0 0 L-41.568 -24 L-41.568 24 L0 48 Z" fill="white" fill-opacity="0.2" stroke="white" stroke-width="3" stroke-linejoin="round"/>
<!-- Right face - simplified, no grid cells -->
<path d="M0 0 L41.568 -24 L41.568 24 L0 48 Z" fill="white" fill-opacity="0.35" stroke="white" stroke-width="3" stroke-linejoin="round"/>
<!-- Top face - open -->
<path d="M0 -48 L-41.568 -24 L0 0 L41.568 -24 Z" fill="none" stroke="white" stroke-width="3" stroke-linejoin="round"/>
<!-- OpenCode logo on top face -->
<g transform="matrix(0.866, 0.5, -0.866, 0.5, 0, -24) scale(0.75)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-16 -20 L16 -20 L16 20 L-16 20 Z M-8 -12 L-8 12 L8 12 L8 -12 Z" fill="white"/>
<path d="M-8 -4 L8 -4 L8 12 L-8 12 Z" fill="white" fill-opacity="0.4"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More