@@ -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"]
|
||||
@@ -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} .'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,2 +0,0 @@
|
||||
# .github/CODEOWNERS
|
||||
* @btriapitsyn
|
||||
@@ -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 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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 +0,0 @@
|
||||
lts
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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`)
|
||||
@@ -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 auto‑send (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 auto‑cleanup 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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
[](https://github.com/btriapitsyn/openchamber/stargazers)
|
||||
[](https://github.com/btriapitsyn/openchamber/releases/latest)
|
||||
[](https://opencode.ai)
|
||||
[](https://discord.gg/ZYRSdnwwKA)
|
||||
[](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.
|
||||
|
||||

|
||||
|
||||
<details>
|
||||
<summary>More screenshots</summary>
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
<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
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 |
|
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 |
|
Before Width: | Height: | Size: 973 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 971 KiB |
|
Before Width: | Height: | Size: 560 KiB |
|
Before Width: | Height: | Size: 722 KiB |
|
Before Width: | Height: | Size: 1001 KiB |
|
Before Width: | Height: | Size: 862 KiB |
@@ -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,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
[](https://github.com/btriapitsyn/openchamber/stargazers)
|
||||
[](https://github.com/btriapitsyn/openchamber/releases/latest)
|
||||
[](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
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
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 |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 54 KiB |
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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/)
|
||||
@@ -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
|
||||
@@ -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/" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
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 |
|
Before Width: | Height: | Size: 65 KiB |