feat: import
perso/opencode-openchamber/pipeline/head Something is wrong with the build of this commit
@@ -0,0 +1,38 @@
|
|||||||
|
pipeline {
|
||||||
|
environment {
|
||||||
|
registry = 'https://registry.hub.docker.com'
|
||||||
|
registryCredential = 'dockerhub_jcabillot'
|
||||||
|
dockerImage = 'jcabillot/openchamber'
|
||||||
|
}
|
||||||
|
|
||||||
|
agent any
|
||||||
|
|
||||||
|
triggers {
|
||||||
|
cron('@midnight')
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Clone repository') {
|
||||||
|
steps{
|
||||||
|
checkout scm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Build image') {
|
||||||
|
steps{
|
||||||
|
sh 'docker build --force-rm=true --no-cache=true --pull -f src/Dockerfile -t ${dockerImage} src/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Deploy Image') {
|
||||||
|
steps{
|
||||||
|
script {
|
||||||
|
withCredentials([usernamePassword(credentialsId: 'dockerhub_jcabillot', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
|
||||||
|
sh 'docker login --username ${DOCKER_USER} --password ${DOCKER_PASS}'
|
||||||
|
sh 'docker push ${dockerImage}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
.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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# .github/CODEOWNERS
|
||||||
|
* @btriapitsyn
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
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 }}
|
||||||
|
|
||||||
|
finalize-release:
|
||||||
|
needs: [create-release, build-desktop-macos, publish-npm, combine-manifests]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
|
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 }}
|
||||||
|
run: |
|
||||||
|
node - <<'NODE'
|
||||||
|
(async () => {
|
||||||
|
const tag = `v${process.env.VERSION}`;
|
||||||
|
const repo = process.env.REPOSITORY;
|
||||||
|
|
||||||
|
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 payload = {
|
||||||
|
username: 'OpenChamber Releases',
|
||||||
|
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
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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 }}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# 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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
lts
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
---
|
||||||
|
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`
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
---
|
||||||
|
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/`
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
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`
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# 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` is a thin Tauri shell that starts the web server sidecar and loads the web UI from `http://127.0.0.1:<port>`.
|
||||||
|
- All backend logic lives in `packages/web/server/*` (and `packages/vscode/*` for the VS Code runtime). Desktop Rust is not a feature backend.
|
||||||
|
- Tauri is used only for stable native integrations: menu, dialog (open folder), notifications, updater, deep-links.
|
||||||
|
|
||||||
|
## 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: Radix UI (`package.json` deps), HeroUI (`package.json` deps), Remixicon (`package.json` deps)
|
||||||
|
- Server: Express (`packages/web/server/index.js`)
|
||||||
|
- Desktop: 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 app (Tauri): `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: `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: Tauri entry `packages/desktop/src-tauri/src/main.rs` (spawns web server sidecar + loads web UI)
|
||||||
|
- Tauri backend: `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`, `bun run build` before finalizing changes).
|
||||||
|
|
||||||
|
## Development rules
|
||||||
|
- Keep diffs tight; avoid drive-by refactors.
|
||||||
|
- Backend changes: keep web/desktop/vscode runtimes consistent (if relevant).
|
||||||
|
- Follow local precedent; search nearby code first.
|
||||||
|
- TypeScript: avoid `any`/blind casts; keep ESLint/TS green.
|
||||||
|
- React: prefer function components + hooks; class only when needed (e.g. error boundaries).
|
||||||
|
- Control flow: avoid nested ternaries; prefer early returns + `if/else`/`switch`.
|
||||||
|
- Styling: Tailwind v4; typography via `packages/ui/src/lib/typography.ts`; theme vars via `packages/ui/src/lib/theme/`.
|
||||||
|
- Shared UI patterns: for "series of items + divider + series of items" layouts, use shared UI primitives instead of duplicating ad-hoc markup in feature components.
|
||||||
|
- Toasts: use custom toast wrapper from `@/components/ui` (backed by `packages/ui/src/components/ui/toast.ts`); do not import `sonner` directly in feature code.
|
||||||
|
- No new deps unless asked.
|
||||||
|
- Never add secrets (`.env`, keys) or log sensitive data.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Recent changes
|
||||||
|
- Releases + high-level changes: `CHANGELOG.md`
|
||||||
|
- Recent commits: `git log --oneline` (latest tags: `v1.4.6`, `v1.4.5`)
|
||||||
@@ -0,0 +1,827 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [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.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# 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).
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# 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"]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
# <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`).
|
||||||
|
|
||||||
|
### 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
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 870 B |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 973 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 971 KiB |
|
After Width: | Height: | Size: 560 KiB |
|
After Width: | Height: | Size: 722 KiB |
|
After Width: | Height: | Size: 1001 KiB |
|
After Width: | Height: | Size: 862 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/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();
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
"name": "openchamber-monorepo",
|
||||||
|
"version": "1.9.1",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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.1",
|
||||||
|
"@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.3.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# <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
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<!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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-pulse {
|
||||||
|
animation: logo-pulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
</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 class="logo-pulse" 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>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "@openchamber/desktop",
|
||||||
|
"version": "1.9.1",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
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}`);
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#!/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);
|
||||||
|
});
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
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));
|
||||||
|
});
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#!/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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
[package]
|
||||||
|
name = "openchamber-desktop"
|
||||||
|
version = "1.9.1"
|
||||||
|
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", "blocking"] }
|
||||||
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
|
serde_json = "1.0.143"
|
||||||
|
tauri = { version = "2.9.4", features = ["macos-private-api"] }
|
||||||
|
tauri-plugin-dialog = "2.4.2"
|
||||||
|
tauri-plugin-log = "2.7.1"
|
||||||
|
tauri-plugin-shell = "2.3.3"
|
||||||
|
tauri-plugin-notification = "2.3.3"
|
||||||
|
tauri-plugin-updater = "2"
|
||||||
|
tokio = { version = "1.38", features = ["rt-multi-thread", "time"] }
|
||||||
|
url = "2.5"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2.5.3", features = [] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
window-vibrancy = "0.7.1"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build();
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,33 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"productName": "OpenChamber",
|
||||||
|
"version": "1.9.1",
|
||||||
|
"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",
|
||||||
|
"transparent": true,
|
||||||
|
"width": 1280,
|
||||||
|
"height": 800,
|
||||||
|
"resizable": true,
|
||||||
|
"fullscreen": false,
|
||||||
|
"decorations": true,
|
||||||
|
"hiddenTitle": true,
|
||||||
|
"titleBarStyle": "Overlay",
|
||||||
|
"trafficLightPosition": {
|
||||||
|
"x": 17,
|
||||||
|
"y": 26
|
||||||
|
},
|
||||||
|
"dragDropEnabled": false,
|
||||||
|
"visible": false,
|
||||||
|
"backgroundThrottling": "disabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"bundle": {
|
||||||
|
"icon": [
|
||||||
|
"icons/dev-icon.icns",
|
||||||
|
"icons/dev-icon.png"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 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).
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
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)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"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": "Troubleshooting", "link": "/troubleshooting/" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"name": "@openchamber/ui",
|
||||||
|
"version": "1.9.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/main.tsx",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsc --noEmit --watch",
|
||||||
|
"build": "tsc --noEmit",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../../eslint.config.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.20.0",
|
||||||
|
"@codemirror/commands": "^6.10.1",
|
||||||
|
"@codemirror/lang-cpp": "^6.0.3",
|
||||||
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
|
"@codemirror/lang-go": "^6.0.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-sql": "^6.10.0",
|
||||||
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
|
"@codemirror/language": "^6.12.1",
|
||||||
|
"@codemirror/language-data": "^6.5.2",
|
||||||
|
"@codemirror/legacy-modes": "^6.5.2",
|
||||||
|
"@codemirror/lint": "^6.9.2",
|
||||||
|
"@codemirror/search": "^6.6.0",
|
||||||
|
"@codemirror/state": "^6.5.4",
|
||||||
|
"@codemirror/view": "^6.39.13",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||||
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
|
"@ibm/plex": "^6.4.1",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"@opencode-ai/sdk": "^1.3.0",
|
||||||
|
"@pierre/diffs": "1.1.0-beta.13",
|
||||||
|
"@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-switch": "^1.2.6",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@remixicon/react": "^4.7.0",
|
||||||
|
"@streamdown/code": "^1.0.2",
|
||||||
|
"@tanstack/react-virtual": "^3.13.18",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
|
"beautiful-mermaid": "^1.1.3",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"codemirror-lang-elixir": "^4.0.0",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
|
"ghostty-web": "^0.4.0",
|
||||||
|
"heic2any": "^0.0.4",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
|
"http-proxy-middleware": "^3.0.5",
|
||||||
|
"motion": "^12.23.24",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"prismjs": "^1.30.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"react-syntax-highlighter": "^15.6.6",
|
||||||
|
"simple-git": "^3.28.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"streamdown": "^2.2.0",
|
||||||
|
"strip-json-comments": "^5.0.3",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"yaml": "^2.8.1",
|
||||||
|
"zod": "^4.3.6",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.33.0",
|
||||||
|
"@tailwindcss/postcss": "^4.0.0",
|
||||||
|
"@tauri-apps/api": "^2.9.0",
|
||||||
|
"@types/node": "^24.3.1",
|
||||||
|
"@types/prismjs": "^1.26.6",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
|
"@types/react": "^19.1.10",
|
||||||
|
"@types/react-dom": "^19.1.7",
|
||||||
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"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",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
@@ -0,0 +1,556 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { MainLayout } from '@/components/layout/MainLayout';
|
||||||
|
import { VSCodeLayout } from '@/components/layout/VSCodeLayout';
|
||||||
|
import { AgentManagerView } from '@/components/views/agent-manager';
|
||||||
|
import { ChatView } from '@/components/views';
|
||||||
|
import { FireworksProvider } from '@/contexts/FireworksContext';
|
||||||
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
|
import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel';
|
||||||
|
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
||||||
|
import { useEventStream } from '@/hooks/useEventStream';
|
||||||
|
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
||||||
|
import { useMenuActions } from '@/hooks/useMenuActions';
|
||||||
|
import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap';
|
||||||
|
import { useServerSessionStatus } from '@/hooks/useServerSessionStatus';
|
||||||
|
import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
|
||||||
|
import { useQueuedMessageAutoSend } from '@/hooks/useQueuedMessageAutoSend';
|
||||||
|
import { useRouter } from '@/hooks/useRouter';
|
||||||
|
import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
|
||||||
|
import { usePwaManifestSync } from '@/hooks/usePwaManifestSync';
|
||||||
|
import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt';
|
||||||
|
import { useWindowTitle } from '@/hooks/useWindowTitle';
|
||||||
|
import { useGitHubPrBackgroundTracking } from '@/hooks/useGitHubPrBackgroundTracking';
|
||||||
|
import { GitPollingProvider } from '@/hooks/useGitPolling';
|
||||||
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
|
import { hasModifier } from '@/lib/utils';
|
||||||
|
import { isDesktopLocalOriginActive, isDesktopShell } from '@/lib/desktop';
|
||||||
|
import { OnboardingScreen } from '@/components/onboarding/OnboardingScreen';
|
||||||
|
import { useSessionStore } from '@/stores/useSessionStore';
|
||||||
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
|
import { opencodeClient } from '@/lib/opencode/client';
|
||||||
|
import { useFontPreferences } from '@/hooks/useFontPreferences';
|
||||||
|
import { CODE_FONT_OPTION_MAP, DEFAULT_MONO_FONT, DEFAULT_UI_FONT, UI_FONT_OPTION_MAP } from '@/lib/fontOptions';
|
||||||
|
import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay';
|
||||||
|
import { AboutDialog } from '@/components/ui/AboutDialog';
|
||||||
|
import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider';
|
||||||
|
import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||||
|
import { VoiceProvider } from '@/components/voice';
|
||||||
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
|
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||||
|
import type { RuntimeAPIs } from '@/lib/api/types';
|
||||||
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
|
const CLI_MISSING_ERROR_REGEX =
|
||||||
|
/ENOENT|spawn\s+opencode|Unable\s+to\s+locate\s+the\s+opencode\s+CLI|OpenCode\s+CLI\s+not\s+found|opencode(\.exe)?\s+not\s+found|opencode(\.exe)?:\s*command\s+not\s+found|not\s+recognized\s+as\s+an\s+internal\s+or\s+external\s+command|env:\s*['"]?(node|bun)['"]?:\s*No\s+such\s+file\s+or\s+directory|(node|bun):\s*No\s+such\s+file\s+or\s+directory/i;
|
||||||
|
const CLI_ONBOARDING_HEALTH_POLL_MS = 1500;
|
||||||
|
|
||||||
|
const AboutDialogWrapper: React.FC = () => {
|
||||||
|
const { isAboutDialogOpen, setAboutDialogOpen } = useUIStore();
|
||||||
|
return (
|
||||||
|
<AboutDialog
|
||||||
|
open={isAboutDialogOpen}
|
||||||
|
onOpenChange={setAboutDialogOpen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type AppProps = {
|
||||||
|
apis: RuntimeAPIs;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmbeddedSessionChatConfig = {
|
||||||
|
sessionId: string;
|
||||||
|
directory: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmbeddedVisibilityPayload = {
|
||||||
|
visible?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readEmbeddedSessionChatConfig = (): EmbeddedSessionChatConfig | null => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (params.get('ocPanel') !== 'session-chat') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionIdRaw = params.get('sessionId');
|
||||||
|
const sessionId = typeof sessionIdRaw === 'string' ? sessionIdRaw.trim() : '';
|
||||||
|
if (!sessionId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const directoryRaw = params.get('directory');
|
||||||
|
const directory = typeof directoryRaw === 'string' && directoryRaw.trim().length > 0
|
||||||
|
? directoryRaw.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
directory,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function App({ apis }: AppProps) {
|
||||||
|
const { initializeApp, isInitialized, isConnected } = useConfigStore();
|
||||||
|
const providersCount = useConfigStore((state) => state.providers.length);
|
||||||
|
const agentsCount = useConfigStore((state) => state.agents.length);
|
||||||
|
const loadProviders = useConfigStore((state) => state.loadProviders);
|
||||||
|
const loadAgents = useConfigStore((state) => state.loadAgents);
|
||||||
|
const { error, clearError, loadSessions } = useSessionStore();
|
||||||
|
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
||||||
|
const setCurrentSession = useSessionStore((state) => state.setCurrentSession);
|
||||||
|
const sessions = useSessionStore((state) => state.sessions);
|
||||||
|
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
||||||
|
const setDirectory = useDirectoryStore((state) => state.setDirectory);
|
||||||
|
const isSwitchingDirectory = useDirectoryStore((state) => state.isSwitchingDirectory);
|
||||||
|
const [showMemoryDebug, setShowMemoryDebug] = React.useState(false);
|
||||||
|
const { uiFont, monoFont } = useFontPreferences();
|
||||||
|
const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus);
|
||||||
|
const [isVSCodeRuntime, setIsVSCodeRuntime] = React.useState<boolean>(() => apis.runtime.isVSCode);
|
||||||
|
const [showCliOnboarding, setShowCliOnboarding] = React.useState(false);
|
||||||
|
const [isEmbeddedVisible, setIsEmbeddedVisible] = React.useState(true);
|
||||||
|
const isDesktopRuntime = React.useMemo(() => isDesktopShell(), []);
|
||||||
|
const appReadyDispatchedRef = React.useRef(false);
|
||||||
|
const embeddedSessionChat = React.useMemo<EmbeddedSessionChatConfig | null>(() => readEmbeddedSessionChatConfig(), []);
|
||||||
|
const embeddedBackgroundWorkEnabled = !embeddedSessionChat || isEmbeddedVisible;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setIsVSCodeRuntime(apis.runtime.isVSCode);
|
||||||
|
}, [apis.runtime.isVSCode]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
registerRuntimeAPIs(apis);
|
||||||
|
return () => registerRuntimeAPIs(null);
|
||||||
|
}, [apis]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (embeddedSessionChat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshGitHubAuthStatus(apis.github, { force: true });
|
||||||
|
}, [apis.github, embeddedSessionChat, refreshGitHubAuthStatus]);
|
||||||
|
|
||||||
|
useGitHubPrBackgroundTracking(embeddedBackgroundWorkEnabled ? apis.github : undefined, apis.git);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof document === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const root = document.documentElement;
|
||||||
|
const uiStack = UI_FONT_OPTION_MAP[uiFont]?.stack ?? UI_FONT_OPTION_MAP[DEFAULT_UI_FONT].stack;
|
||||||
|
const monoStack = CODE_FONT_OPTION_MAP[monoFont]?.stack ?? CODE_FONT_OPTION_MAP[DEFAULT_MONO_FONT].stack;
|
||||||
|
|
||||||
|
root.style.setProperty('--font-sans', uiStack);
|
||||||
|
root.style.setProperty('--font-heading', uiStack);
|
||||||
|
root.style.setProperty('--font-family-sans', uiStack);
|
||||||
|
root.style.setProperty('--font-mono', monoStack);
|
||||||
|
root.style.setProperty('--font-family-mono', monoStack);
|
||||||
|
root.style.setProperty('--ui-regular-font-weight', '400');
|
||||||
|
|
||||||
|
if (document.body) {
|
||||||
|
document.body.style.fontFamily = uiStack;
|
||||||
|
}
|
||||||
|
}, [uiFont, monoFont]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isInitialized) {
|
||||||
|
const hideInitialLoading = () => {
|
||||||
|
const loadingElement = document.getElementById('initial-loading');
|
||||||
|
if (loadingElement) {
|
||||||
|
loadingElement.classList.add('fade-out');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
loadingElement.remove();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timer = setTimeout(hideInitialLoading, 150);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isInitialized]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const fallbackTimer = setTimeout(() => {
|
||||||
|
const loadingElement = document.getElementById('initial-loading');
|
||||||
|
if (loadingElement && !isInitialized) {
|
||||||
|
loadingElement.classList.add('fade-out');
|
||||||
|
setTimeout(() => {
|
||||||
|
loadingElement.remove();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearTimeout(fallbackTimer);
|
||||||
|
}, [isInitialized]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const init = async () => {
|
||||||
|
// VS Code runtime bootstraps config + sessions after the managed OpenCode instance reports "connected".
|
||||||
|
// Doing the default initialization here can race with startup and lead to one-shot failures.
|
||||||
|
if (isVSCodeRuntime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await initializeApp();
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
}, [initializeApp, isVSCodeRuntime]);
|
||||||
|
|
||||||
|
const startupRecoveryInProgressRef = React.useRef(false);
|
||||||
|
const startupRecoveryLastAttemptRef = React.useRef(0);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isVSCodeRuntime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (providersCount > 0 && agentsCount > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (startupRecoveryInProgressRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - startupRecoveryLastAttemptRef.current < 750) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startupRecoveryLastAttemptRef.current = now;
|
||||||
|
startupRecoveryInProgressRef.current = true;
|
||||||
|
|
||||||
|
const repair = async () => {
|
||||||
|
try {
|
||||||
|
if (providersCount === 0) {
|
||||||
|
await loadProviders();
|
||||||
|
}
|
||||||
|
if (agentsCount === 0) {
|
||||||
|
await loadAgents();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Keep UI responsive; we'll retry on next cycle.
|
||||||
|
} finally {
|
||||||
|
startupRecoveryInProgressRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void repair();
|
||||||
|
}, [agentsCount, isConnected, isVSCodeRuntime, loadAgents, loadProviders, providersCount]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isSwitchingDirectory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncDirectoryAndSessions = async () => {
|
||||||
|
// VS Code runtime loads sessions via VSCodeLayout bootstrap to avoid startup races.
|
||||||
|
if (isVSCodeRuntime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
opencodeClient.setDirectory(currentDirectory);
|
||||||
|
|
||||||
|
await loadSessions();
|
||||||
|
};
|
||||||
|
|
||||||
|
syncDirectoryAndSessions();
|
||||||
|
}, [currentDirectory, isSwitchingDirectory, loadSessions, isConnected, isVSCodeRuntime]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!embeddedSessionChat || typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyVisibility = (payload?: EmbeddedVisibilityPayload) => {
|
||||||
|
const nextVisible = payload?.visible === true;
|
||||||
|
setIsEmbeddedVisible(nextVisible);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
if (event.origin !== window.location.origin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = event.data as { type?: unknown; payload?: EmbeddedVisibilityPayload };
|
||||||
|
if (data?.type !== 'openchamber:embedded-visibility') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyVisibility(data.payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scopedWindow = window as unknown as {
|
||||||
|
__openchamberSetEmbeddedVisibility?: (payload?: EmbeddedVisibilityPayload) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
scopedWindow.__openchamberSetEmbeddedVisibility = applyVisibility;
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
if (scopedWindow.__openchamberSetEmbeddedVisibility === applyVisibility) {
|
||||||
|
delete scopedWindow.__openchamberSetEmbeddedVisibility;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [embeddedSessionChat]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!embeddedSessionChat?.directory || isVSCodeRuntime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDirectory === embeddedSessionChat.directory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDirectory(embeddedSessionChat.directory, { showOverlay: false });
|
||||||
|
}, [currentDirectory, embeddedSessionChat, isVSCodeRuntime, setDirectory]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!embeddedSessionChat || isVSCodeRuntime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSessionId === embeddedSessionChat.sessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessions.some((session) => session.id === embeddedSessionChat.sessionId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCurrentSession(embeddedSessionChat.sessionId);
|
||||||
|
}, [currentSessionId, embeddedSessionChat, isVSCodeRuntime, sessions, setCurrentSession]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!embeddedSessionChat || typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStorage = (event: StorageEvent) => {
|
||||||
|
if (event.storageArea !== window.localStorage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key !== 'ui-store') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void useUIStore.persist.rehydrate();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('storage', handleStorage);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('storage', handleStorage);
|
||||||
|
};
|
||||||
|
}, [embeddedSessionChat]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
if (!isInitialized || isSwitchingDirectory) return;
|
||||||
|
if (appReadyDispatchedRef.current) return;
|
||||||
|
appReadyDispatchedRef.current = true;
|
||||||
|
(window as unknown as { __openchamberAppReady?: boolean }).__openchamberAppReady = true;
|
||||||
|
window.dispatchEvent(new Event('openchamber:app-ready'));
|
||||||
|
}, [isInitialized, isSwitchingDirectory]);
|
||||||
|
|
||||||
|
useEventStream({ enabled: embeddedBackgroundWorkEnabled });
|
||||||
|
|
||||||
|
// Server-authoritative session status polling
|
||||||
|
// Replaces SSE-dependent status updates with reliable HTTP polling
|
||||||
|
useServerSessionStatus({ enabled: embeddedBackgroundWorkEnabled });
|
||||||
|
|
||||||
|
usePushVisibilityBeacon({ enabled: embeddedBackgroundWorkEnabled });
|
||||||
|
usePwaManifestSync();
|
||||||
|
usePwaInstallPrompt();
|
||||||
|
|
||||||
|
useWindowTitle();
|
||||||
|
|
||||||
|
useRouter();
|
||||||
|
|
||||||
|
useKeyboardShortcuts();
|
||||||
|
|
||||||
|
const handleToggleMemoryDebug = React.useCallback(() => {
|
||||||
|
setShowMemoryDebug(prev => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useMenuActions(handleToggleMemoryDebug);
|
||||||
|
|
||||||
|
useSessionStatusBootstrap({ enabled: embeddedBackgroundWorkEnabled });
|
||||||
|
useSessionAutoCleanup({ enabled: embeddedBackgroundWorkEnabled });
|
||||||
|
useQueuedMessageAutoSend({ enabled: embeddedBackgroundWorkEnabled });
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (embeddedSessionChat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (hasModifier(e) && e.shiftKey && e.key === 'D') {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowMemoryDebug(prev => !prev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [embeddedSessionChat]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (embeddedSessionChat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
|
||||||
|
setTimeout(() => clearError(), 5000);
|
||||||
|
}
|
||||||
|
}, [clearError, embeddedSessionChat, error]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (embeddedSessionChat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDesktopShell() || !isDesktopLocalOriginActive()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const run = async () => {
|
||||||
|
const res = await fetch('/health', { method: 'GET' }).catch(() => null);
|
||||||
|
if (!res || !res.ok || cancelled) return;
|
||||||
|
const data = (await res.json().catch(() => null)) as null | {
|
||||||
|
openCodeRunning?: unknown;
|
||||||
|
isOpenCodeReady?: unknown;
|
||||||
|
opencodeBinaryResolved?: unknown;
|
||||||
|
lastOpenCodeError?: unknown;
|
||||||
|
};
|
||||||
|
if (!data || cancelled) return;
|
||||||
|
const openCodeRunning = data.openCodeRunning === true;
|
||||||
|
const isOpenCodeReady = data.isOpenCodeReady === true;
|
||||||
|
const resolvedBinary = typeof data.opencodeBinaryResolved === 'string' ? data.opencodeBinaryResolved.trim() : '';
|
||||||
|
const hasResolvedBinary = resolvedBinary.length > 0;
|
||||||
|
const err = typeof data.lastOpenCodeError === 'string' ? data.lastOpenCodeError : '';
|
||||||
|
const cliMissing =
|
||||||
|
!openCodeRunning &&
|
||||||
|
(CLI_MISSING_ERROR_REGEX.test(err) || (!hasResolvedBinary && !isOpenCodeReady));
|
||||||
|
setShowCliOnboarding(cliMissing);
|
||||||
|
};
|
||||||
|
|
||||||
|
void run();
|
||||||
|
const interval = window.setInterval(() => {
|
||||||
|
void run();
|
||||||
|
}, CLI_ONBOARDING_HEALTH_POLL_MS);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
window.clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [embeddedSessionChat]);
|
||||||
|
|
||||||
|
const handleCliAvailable = React.useCallback(() => {
|
||||||
|
setShowCliOnboarding(false);
|
||||||
|
window.location.reload();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (showCliOnboarding) {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div className="h-full text-foreground bg-transparent">
|
||||||
|
<OnboardingScreen onCliAvailable={handleCliAvailable} />
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embeddedSessionChat) {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<RuntimeAPIProvider apis={apis}>
|
||||||
|
<TooltipProvider delayDuration={700} skipDelayDuration={150}>
|
||||||
|
<div className="h-full text-foreground bg-background">
|
||||||
|
<ChatView />
|
||||||
|
<Toaster />
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</RuntimeAPIProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// VS Code runtime - simplified layout without git/terminal views
|
||||||
|
if (isVSCodeRuntime) {
|
||||||
|
// Check if this is the Agent Manager panel
|
||||||
|
const panelType = typeof window !== 'undefined'
|
||||||
|
? (window as { __OPENCHAMBER_PANEL_TYPE__?: 'chat' | 'agentManager' }).__OPENCHAMBER_PANEL_TYPE__
|
||||||
|
: 'chat';
|
||||||
|
|
||||||
|
if (panelType === 'agentManager') {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<RuntimeAPIProvider apis={apis}>
|
||||||
|
<TooltipProvider delayDuration={700} skipDelayDuration={150}>
|
||||||
|
<div className="h-full text-foreground bg-background">
|
||||||
|
<AgentManagerView />
|
||||||
|
<Toaster />
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</RuntimeAPIProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<RuntimeAPIProvider apis={apis}>
|
||||||
|
<FireworksProvider>
|
||||||
|
<TooltipProvider delayDuration={700} skipDelayDuration={150}>
|
||||||
|
<div className="h-full text-foreground bg-background">
|
||||||
|
<VSCodeLayout />
|
||||||
|
<Toaster />
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</FireworksProvider>
|
||||||
|
</RuntimeAPIProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<RuntimeAPIProvider apis={apis}>
|
||||||
|
<GitPollingProvider>
|
||||||
|
<FireworksProvider>
|
||||||
|
<VoiceProvider>
|
||||||
|
<TooltipProvider delayDuration={700} skipDelayDuration={150}>
|
||||||
|
<div className={isDesktopRuntime ? 'h-full text-foreground bg-transparent' : 'h-full text-foreground bg-background'}>
|
||||||
|
<MainLayout />
|
||||||
|
<Toaster />
|
||||||
|
<ConfigUpdateOverlay />
|
||||||
|
<AboutDialogWrapper />
|
||||||
|
{showMemoryDebug && (
|
||||||
|
<MemoryDebugPanel onClose={() => setShowMemoryDebug(false)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</VoiceProvider>
|
||||||
|
</FireworksProvider>
|
||||||
|
</GitPollingProvider>
|
||||||
|
</RuntimeAPIProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#29b6f6" d="M21 16.5c0 .38-.21.71-.53.88l-7.9 4.44c-.16.12-.36.18-.57.18s-.41-.06-.57-.18l-7.9-4.44A.99.99 0 0 1 3 16.5v-9c0-.38.21-.71.53-.88l7.9-4.44c.16-.12.36-.18.57-.18s.41.06.57.18l7.9 4.44c.32.17.53.5.53.88zM12 4.15 6.04 7.5 12 10.85l5.96-3.35zM5 15.91l6 3.38v-6.71L5 9.21zm14 0v-6.7l-6 3.37v6.71z"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,26 @@
|
|||||||
|
# File Type Icons Sprite
|
||||||
|
|
||||||
|
This directory keeps the source file-type SVG icons and the generated sprite used by the UI.
|
||||||
|
|
||||||
|
## Runtime behavior
|
||||||
|
|
||||||
|
- The UI resolves an icon id in `packages/ui/src/lib/fileTypeIcons.ts`.
|
||||||
|
- Valid icon ids are loaded from `packages/ui/src/lib/fileTypeIconIds.ts`.
|
||||||
|
- `packages/ui/src/components/icons/FileTypeIcon.tsx` renders the icon with `<use href="...#icon-id" />` from `sprite.svg`.
|
||||||
|
- Vite handles `sprite.svg` as a normal asset URL automatically.
|
||||||
|
|
||||||
|
The sprite generator rewrites internal SVG ids per icon (gradients, clip paths, filters) so ids do not collide after packing all icons into one file.
|
||||||
|
|
||||||
|
## Build step
|
||||||
|
|
||||||
|
- No special step is required for normal `dev`/`build`.
|
||||||
|
- Regenerate the sprite only when icon source files in this folder change:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run icons:sprite
|
||||||
|
```
|
||||||
|
|
||||||
|
The command regenerates both `sprite.svg` and `packages/ui/src/lib/fileTypeIconIds.ts`.
|
||||||
|
Both generated files are committed and consumed automatically by app builds.
|
||||||
|
|
||||||
|
If you only run `bun run dev`, `bun run build`, `bun run lint`, or `bun run type-check`, no extra sprite step is needed unless the source icon files changed.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#0288d1" d="M2 10v12h14l14-12"/></svg>
|
||||||
|
After Width: | Height: | Size: 110 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#ff5722" d="M13.295 11.033V7.65l2.126-2.136c.774-.763.919-1.981.377-2.929a2.38 2.38 0 0 0-2.068-1.217c-.203 0-.435.029-.619.087-1.044.28-1.749 1.246-1.749 2.33v3.13L8.327 9.98a5.75 5.75 0 0 0-1.208 6.214 5.62 5.62 0 0 0 4.243 3.432v.59a.5.5 0 0 1-.483.482h-1.45v1.934h1.45a2.43 2.43 0 0 0 2.416-2.417v-.483c1.962 0 4.02-1.856 4.02-4.591 0-2.223-1.855-4.108-4.02-4.108m0-7.249c0-.222.106-.396.31-.454a.47.47 0 0 1 .54.222.48.48 0 0 1-.077.59l-.773.83V3.785m-1.933 7.732c-.938.619-1.643 1.682-1.894 2.668l1.894.503v2.948a3.73 3.73 0 0 1-2.484-2.185 3.8 3.8 0 0 1 .802-4.098l1.682-1.769zm1.933 6.283v-4.89c1.13 0 2.107 1.062 2.107 2.232 0 1.691-1.227 2.658-2.107 2.658"/></svg>
|
||||||
|
After Width: | Height: | Size: 746 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="#f44336" d="M560-160v-80h120q17 0 28.5-11.5T720-280v-80q0-38 22-69t58-44v-14q-36-13-58-44t-22-69v-80q0-17-11.5-28.5T680-720H560v-80h120q50 0 85 35t35 85v80q0 17 11.5 28.5T840-560h40v160h-40q-17 0-28.5 11.5T800-360v80q0 50-35 85t-85 35zm-280 0q-50 0-85-35t-35-85v-80q0-17-11.5-28.5T120-400H80v-160h40q17 0 28.5-11.5T160-600v-80q0-50 35-85t85-35h120v80H280q-17 0-28.5 11.5T240-680v80q0 38-22 69t-58 44v14q36 13 58 44t22 69v80q0 17 11.5 28.5T280-240h120v80z"/><path fill="#f44336" d="M360-600h80v40h-80zm80 240h40v-200h-40v80h-80v-80h-40v200h40v-80h80zm200-200v-40H530a10 10 0 0 0-10 10v100a10 10 0 0 0 10 10h70v80h-80v40h110a10 10 0 0 0 10-10v-140a10 10 0 0 0-10-10h-70v-40z"/></svg>
|
||||||
|
After Width: | Height: | Size: 758 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#0277bd" d="m2 12 2.9-1.07c.25-1.1.87-1.73.87-1.73a3.996 3.996 0 0 1 5.65 0l1.41 1.41 6.31-6.7c.95 3.81 0 7.62-2.33 10.69L22 19.62s-8.47 1.9-13.4-1.95c-2.63-2.06-3.22-3.26-3.59-4.52zm5.04.21c.37.37.98.37 1.35 0s.37-.97 0-1.34a.96.96 0 0 0-1.35 0c-.37.37-.37.97 0 1.34"/></svg>
|
||||||
|
After Width: | Height: | Size: 348 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="28" height="28" x="2" y="2" fill="#5d4037" rx="4"/><path fill="#ffb74d" d="M20.988 9.999a.96.96 0 0 1-.687-.269 1 1 0 0 1-.263-.704.9.9 0 0 1 .278-.681 1 1 0 0 1 .687-.268.93.93 0 0 1 .703.268 1.046 1.046 0 0 1-.015 1.385.9.9 0 0 1-.703.268M20 12h2v10h-2zm-5.63-1.98-.01-.02h-2.08a.12.12 0 0 0-.1.13 4.5 4.5 0 0 1-.06.74c-.05.13-.08.26-.12.37l-.27.78L8 22h2.14l.75-2h5.24l.79 2h2.16zM11.64 18l1.8-4.84.01.04.02.04L14.95 17l.39 1z"/></svg>
|
||||||
|
After Width: | Height: | Size: 511 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="28" height="28" x="2" y="2" fill="#795548" rx="4"/><path fill="#ffb74d" d="M20.988 9.999a.96.96 0 0 1-.687-.269 1 1 0 0 1-.263-.704.9.9 0 0 1 .278-.681 1 1 0 0 1 .687-.268.93.93 0 0 1 .703.268 1.046 1.046 0 0 1-.015 1.385.9.9 0 0 1-.703.268M20 12h2v10h-2zm-5.63-1.98-.01-.02h-2.08a.12.12 0 0 0-.1.13 4.5 4.5 0 0 1-.06.74c-.05.13-.08.26-.12.37l-.27.78L8 22h2.14l.75-2h5.24l.79 2h2.16zM11.64 18l1.8-4.84.01.04.02.04L14.95 17l.39 1z"/></svg>
|
||||||
|
After Width: | Height: | Size: 511 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="28" height="28" x="2" y="2" fill="#37474f" rx="4"/><path fill="#64b5f6" d="M23.744 14.716a3.7 3.7 0 0 0-1.066-.408 5.4 5.4 0 0 0-1.245-.157 2.1 2.1 0 0 0-.666.085.57.57 0 0 0-.345.24.7.7 0 0 0-.089.324.56.56 0 0 0 .111.313 1.3 1.3 0 0 0 .378.324q.386.217.79.397a7.8 7.8 0 0 1 1.71.877 2.7 2.7 0 0 1 .878.998 2.8 2.8 0 0 1 .256 1.238 2.96 2.96 0 0 1-.434 1.599 2.83 2.83 0 0 1-1.244 1.07 4.75 4.75 0 0 1-2.011.384 7 7 0 0 1-1.511-.156 4.2 4.2 0 0 1-1.134-.385.24.24 0 0 1-.122-.228v-2.092a.14.14 0 0 1 .044-.108c.034-.024.067-.012.1.012a4.6 4.6 0 0 0 1.378.59 4.8 4.8 0 0 0 1.311.18 2 2 0 0 0 .923-.169.56.56 0 0 0 .3-.505.65.65 0 0 0-.267-.48 4.6 4.6 0 0 0-1.089-.565 6.6 6.6 0 0 1-1.578-.866 3 3 0 0 1-.844-1.021 2.76 2.76 0 0 1-.256-1.226 3 3 0 0 1 .378-1.455 2.8 2.8 0 0 1 1.167-1.105A4 4 0 0 1 21.533 12a9 9 0 0 1 1.378.108 3.7 3.7 0 0 1 .956.277.2.2 0 0 1 .11.108.7.7 0 0 1 .023.144v1.96a.15.15 0 0 1-.056.12.28.28 0 0 1-.2 0M12.38 10H9.99v-.03h-2v12h2V18h2.39A3.62 3.62 0 0 0 16 14.38v-.76A3.62 3.62 0 0 0 12.38 10M14 14.38A1.626 1.626 0 0 1 12.38 16H9.99v-4h2.39A1.626 1.626 0 0 1 14 13.62Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="28" height="28" x="2" y="2" fill="#455a64" rx="4"/><path fill="#64b5f6" d="M23.744 14.716a3.7 3.7 0 0 0-1.066-.408 5.4 5.4 0 0 0-1.245-.157 2.1 2.1 0 0 0-.666.085.57.57 0 0 0-.345.24.7.7 0 0 0-.089.324.56.56 0 0 0 .111.313 1.3 1.3 0 0 0 .378.324q.386.217.79.397a7.8 7.8 0 0 1 1.71.877 2.7 2.7 0 0 1 .878.998 2.8 2.8 0 0 1 .256 1.238 2.96 2.96 0 0 1-.434 1.599 2.83 2.83 0 0 1-1.244 1.07 4.75 4.75 0 0 1-2.011.384 7 7 0 0 1-1.511-.156 4.2 4.2 0 0 1-1.134-.385.24.24 0 0 1-.122-.228v-2.092a.14.14 0 0 1 .044-.108c.034-.024.067-.012.1.012a4.6 4.6 0 0 0 1.378.59 4.8 4.8 0 0 0 1.311.18 2 2 0 0 0 .923-.169.56.56 0 0 0 .3-.505.65.65 0 0 0-.267-.48 4.6 4.6 0 0 0-1.089-.565 6.6 6.6 0 0 1-1.578-.866 3 3 0 0 1-.844-1.021 2.76 2.76 0 0 1-.256-1.226 3 3 0 0 1 .378-1.455 2.8 2.8 0 0 1 1.167-1.105A4 4 0 0 1 21.533 12a9 9 0 0 1 1.378.108 3.7 3.7 0 0 1 .956.277.2.2 0 0 1 .11.108.7.7 0 0 1 .023.144v1.96a.15.15 0 0 1-.056.12.28.28 0 0 1-.2 0M12.38 10H9.99v-.03h-2v12h2V18h2.39A3.62 3.62 0 0 0 16 14.38v-.76A3.62 3.62 0 0 0 12.38 10M14 14.38A1.626 1.626 0 0 1 12.38 16H9.99v-4h2.39A1.626 1.626 0 0 1 14 13.62Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#e53935" d="M4 5v22a1 1 0 0 0 1 1h22a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1m20 7c-2.926 0-4.21.722-5.012 2H22v4h-4.582C16.34 20.857 14.393 24 8 24v-4c4.559 0 5.14-1.744 6.103-4.632C15.139 12.258 16.559 8 24 8Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 297 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path fill="#7c4dff" d="m79.579 25.741-66.481 115.15h63.305l11.218-19.433H47.613L79.804 65.7l20.005 34.649 11.423-19.783zm42.118 50.221-45.203 78.297h90.408z" paint-order="fill markers stroke"/></svg>
|
||||||
|
After Width: | Height: | Size: 262 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#7986cb" fill-rule="evenodd" d="M6.752 1.158C2.234 1.96-.271 6.943 1.758 11.09c2.537 5.185 10.047 5.142 12.511-.07C16.69 5.9 12.321.17 6.752 1.159m.587 2.335c2.576.517 5.233 1.323 5.326 1.615.26.808.256 4.849-.004 5.34-.066.125-1.209-.012-2.08-.247l-.351-.094-.001-.437c-.005-1.308-.138-2.547-.29-2.7-.176-.176-1.312-.545-3.052-.99L5.78 5.697l-.014-.267c-.033-.6.117-1.95.232-2.093.063-.079.315-.05 1.34.157M4.029 5.39c.5.066 1.083.178 1.492.289l.178.048.03.984c.058 1.844.117 2.13.475 2.29.448.2 2.083.679 3.62 1.061l.34.084-.01.653c-.012.735-.083 1.393-.175 1.617l-.062.15-.261-.03c-.976-.113-4.175-.896-5.567-1.362-.611-.205-.759-.284-.811-.435-.23-.66-.23-4.905 0-5.337.054-.1.08-.1.75-.012"/></svg>
|
||||||
|
After Width: | Height: | Size: 775 B |