refactor: remove legacy Tauri desktop support

Electron updater now uses Electron release metadata only
Removed legacy Tauri package and migration workflow
Replaced Tauri shim usage with the desktop bridge
This commit is contained in:
Bohdan Triapitsyn
2026-06-03 02:42:00 +03:00
parent e80bd15dd9
commit c7bc026b4b
81 changed files with 269 additions and 16914 deletions
-21
View File
@@ -1,21 +0,0 @@
# Vite build output
dist/
# Tauri build artifacts
src-tauri/target/
# Tauri generated code
src-tauri/gen/
# Desktop sidecar + bundled web assets (generated)
src-tauri/resources/web-dist/
src-tauri/sidecars/openchamber-server-*
src-tauri/sidecars/*.exe
!src-tauri/resources/.gitkeep
!src-tauri/sidecars/.gitkeep
# OpenCode CLI state tracking
.opencode-cli-state.json
# OS-specific
.DS_Store
-68
View File
@@ -1,68 +0,0 @@
# <picture><source media="(prefers-color-scheme: dark)" srcset="https://github.com/btriapitsyn/openchamber/raw/HEAD/docs/references/badges/openchamber-logo-dark.svg"><img src="https://github.com/btriapitsyn/openchamber/raw/HEAD/docs/references/badges/openchamber-logo-light.svg" width="32" height="32" align="absmiddle" /></picture> OpenChamber Desktop
[![GitHub stars](https://img.shields.io/github/stars/btriapitsyn/openchamber?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iI2YxZWNlYyIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0yMjkuMDYsMTA4Ljc5bC00OC43LDQyLDE0Ljg4LDYyLjc5YTguNCw4LjQsMCwwLDEtMTIuNTIsOS4xN0wxMjgsMTg5LjA5LDczLjI4LDIyMi43NGE4LjQsOC40LDAsMCwxLTEyLjUyLTkuMTdsMTQuODgtNjIuNzktNDguNy00MkE4LjQ2LDguNDYsMCwwLDEsMzEuNzMsOTRMOTUuNjQsODguOGwyNC42Mi01OS42YTguMzYsOC4zNiwwLDAsMSwxNS40OCwwbDI0LjYyLDU5LjZMMjI0LjI3LDk0QTguNDYsOC40NiwwLDAsMSwyMjkuMDYsMTA4Ljc5WiIgb3BhY2l0eT0iMC4yIj48L3BhdGg%2BPHBhdGggZD0iTTIzOS4xOCw5Ny4yNkExNi4zOCwxNi4zOCwwLDAsMCwyMjQuOTIsODZsLTU5LTQuNzZMMTQzLjE0LDI2LjE1YTE2LjM2LDE2LjM2LDAsMCwwLTMwLjI3LDBMOTAuMTEsODEuMjMsMzEuMDgsODZhMTYuNDYsMTYuNDYsMCwwLDAtOS4zNywyOC44Nmw0NSwzOC44M0w1MywyMTEuNzVhMTYuMzgsMTYuMzgsMCwwLDAsMjQuNSwxNy44MkwxMjgsMTk4LjQ5bDUwLjUzLDMxLjA4QTE2LjQsMTYuNCwwLDAsMCwyMDMsMjExLjc1bC0xMy43Ni01OC4wNyw0NS0zOC44M0ExNi40MywxNi40MywwLDAsMCwyMzkuMTgsOTcuMjZabS0xNS4zNCw1LjQ3LTQ4LjcsNDJhOCw4LDAsMCwwLTIuNTYsNy45MWwxNC44OCw2Mi44YS4zNy4zNywwLDAsMS0uMTcuNDhjLS4xOC4xNC0uMjMuMTEtLjM4LDBsLTU0LjcyLTMzLjY1YTgsOCwwLDAsMC04LjM4LDBMNjkuMDksMjE1Ljk0Yy0uMTUuMDktLjE5LjEyLS4zOCwwYS4zNy4zNywwLDAsMS0uMTctLjQ4bDE0Ljg4LTYyLjhhOCw4LDAsMCwwLTIuNTYtNy45MWwtNDguNy00MmMtLjEyLS4xLS4yMy0uMTktLjEzLS41cy4xOC0uMjcuMzMtLjI5bDYzLjkyLTUuMTZBOCw4LDAsMCwwLDEwMyw5MS44NmwyNC42Mi01OS42MWMuMDgtLjE3LjExLS4yNS4zNS0uMjVzLjI3LjA4LjM1LjI1TDE1Myw5MS44NmE4LDgsMCwwLDAsNi43NSw0LjkybDYzLjkyLDUuMTZjLjE1LDAsLjI0LDAsLjMzLjI5UzIyNCwxMDIuNjMsMjIzLjg0LDEwMi43M1oiPjwvcGF0aD48L3N2Zz4%3D&logoColor=FFFCF0&labelColor=100F0F&color=66800B)](https://github.com/btriapitsyn/openchamber/stargazers)
[![GitHub release](https://img.shields.io/github/v/release/btriapitsyn/openchamber?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iI2YxZWNlYyIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0xMjgsMTI5LjA5VjIzMmE4LDgsMCwwLDEtMy44NC0xbC04OC00OC4xOGE4LDgsMCwwLDEtNC4xNi03VjgwLjE4YTgsOCwwLDAsMSwuNy0zLjI1WiIgb3BhY2l0eT0iMC4yIj48L3BhdGg%2BPHBhdGggZD0iTTIyMy42OCw2Ni4xNSwxMzUuNjgsMThhMTUuODgsMTUuODgsMCwwLDAtMTUuMzYsMGwtODgsNDguMTdhMTYsMTYsMCwwLDAtOC4zMiwxNHY5NS42NGExNiwxNiwwLDAsMCw4LjMyLDE0bDg4LDQ4LjE3YTE1Ljg4LDE1Ljg4LDAsMCwwLDE1LjM2LDBsODgtNDguMTdhMTYsMTYsMCwwLDAsOC4zMi0xNFY4MC4xOEExNiwxNiwwLDAsMCwyMjMuNjgsNjYuMTVaTTEyOCwzMmw4MC4zNCw0NC0yOS43NywxNi4zLTgwLjM1LTQ0Wk0xMjgsMTIwLDQ3LjY2LDc2bDMzLjktMTguNTYsODAuMzQsNDRaTTQwLDkwbDgwLDQzLjc4djg1Ljc5TDQwLDE3NS44MlptMTc2LDg1Ljc4aDBsLTgwLDQzLjc5VjEzMy44MmwzMi0xNy41MVYxNTJhOCw4LDAsMCwwLDE2LDBWMTA3LjU1TDIxNiw5MHY4NS43N1oiPjwvcGF0aD48L3N2Zz4%3D&logoColor=FFFCF0&labelColor=100F0F&color=205EA6)](https://github.com/btriapitsyn/openchamber/releases/latest)
[![Discord](https://img.shields.io/badge/Discord-join.svg?style=flat&labelColor=100F0F&color=8B7EC8&logo=discord&logoColor=FFFCF0)](https://discord.gg/ZYRSdnwwKA)
A native macOS app for [OpenCode](https://opencode.ai). Feels like home - multiple windows, SSH remotes, project actions, and everything running locally.
Full project overview, screenshots, and all features: [github.com/btriapitsyn/openchamber](https://github.com/btriapitsyn/openchamber)
## Install
Download from [Releases](https://github.com/btriapitsyn/openchamber/releases). Available for macOS (Apple Silicon and Intel).
> **Prerequisite:** [OpenCode CLI](https://opencode.ai) installed.
## What makes the desktop app special
- **Remote instances over SSH** - connect to remote OpenChamber servers with dedicated lifecycle and UX flows
- **Project Actions** - run dev servers, configure SSH port forwarding, open remote URLs locally
- **Multi-window** - work on several projects in parallel, each in its own window
- **"Open In" shortcuts** - open workspace in Finder, Terminal, or your editor of choice
- **Local + remote switching** - jump between local and remote OpenChamber instances
- **Native macOS integration** - menus, deep-links, auto-update, and polished window management
Plus everything from the shared OpenChamber UI: branchable timeline, Git sidebar, terminal, voice mode, and more.
## Features
### Core UI
- Branchable chat timeline with `/undo`, `/redo`, and one-click forks from earlier turns
- Smart tool UIs for diffs, file operations, permissions, and long-running task progress
- Multi-agent runs from one prompt with isolated worktrees for safe comparisons
- Git workflows in-app: identities, commits, PR creation, checks, and merge actions
- Context visibility tools (token/cost breakdowns, raw message inspection, and activity summaries)
- Integrated terminal with per-directory sessions and stable performance on heavy output
### Desktop (macOS)
- Native macOS menu integration with polished app actions and deep-link handling
- Multi-window support for parallel project/session workflows
- "Open In" shortcuts for Finder, Terminal, and your preferred editor
- Fast switching between local and remote instances
- Workspace-first startup flow with directory picker and steadier window restore behavior
### Remote Tunnel (Desktop)
- Configure in **Settings -> OpenChamber -> Remote Tunnel**.
- Supported Cloudflare modes: **Quick**, **Managed Remote**, **Managed Local**.
- One active tunnel per Desktop instance. Starting a different mode replaces the current tunnel.
- Replacing or stopping a tunnel revokes existing connect links and invalidates remote tunnel sessions.
- Connect links are one-time tokens; generate a new link for each new connection attempt.
## Development
```bash
git clone https://github.com/btriapitsyn/openchamber.git
cd openchamber
bun install
bun run desktop:dev
```
</details>
## License
MIT
-110
View File
@@ -1,110 +0,0 @@
<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenChamber</title>
<style>
:root {
--splash-background-dark: #151313;
--splash-stroke-dark: #CECDC3;
--splash-background-light: #FFFCF0;
--splash-stroke-light: #100F0F;
--splash-background: var(--splash-background-dark);
--splash-stroke: var(--splash-stroke-dark);
--splash-face-fill: rgba(255, 255, 255, 0.15);
--splash-cell-fill: rgba(255, 255, 255, 0.35);
--splash-logo-fill: var(--splash-stroke);
}
html[data-splash-variant='light'] {
--splash-background: var(--splash-background-light);
--splash-stroke: var(--splash-stroke-light);
--splash-face-fill: rgba(0, 0, 0, 0.15);
--splash-cell-fill: rgba(0, 0, 0, 0.4);
}
html[data-splash-variant='dark'] {
--splash-background: var(--splash-background-dark);
--splash-stroke: var(--splash-stroke-dark);
}
@supports (color: color-mix(in srgb, white 50%, transparent)) {
:root {
--splash-face-fill: color-mix(in srgb, var(--splash-stroke) 15%, transparent);
--splash-cell-fill: color-mix(in srgb, var(--splash-stroke) 35%, transparent);
}
}
@media (prefers-color-scheme: light) {
:root {
--splash-background: var(--splash-background-light);
--splash-stroke: var(--splash-stroke-light);
--splash-face-fill: rgba(0, 0, 0, 0.15);
--splash-cell-fill: rgba(0, 0, 0, 0.4);
}
}
html,
body {
margin: 0;
height: 100%;
}
body {
background-color: var(--splash-background);
color: var(--splash-stroke);
display: flex;
align-items: center;
justify-content: center;
font-family: system-ui, -apple-system, sans-serif;
}
</style>
</head>
<body>
<noscript>OpenChamber requires JavaScript.</noscript>
<svg width="120" height="120" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OpenChamber loading icon">
<path d="M50 50 L8.432 26 L8.432 74 L50 98 Z" fill="var(--splash-face-fill)" stroke="var(--splash-stroke)" stroke-width="2" stroke-linejoin="round"/>
<path d="M50 50 L39.608 44 L39.608 56 L50 62 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M39.608 44 L29.216 38 L29.216 50 L39.608 56 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
<path d="M29.216 38 L18.824 32 L18.824 44 L29.216 50 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
<path d="M18.824 32 L8.432 26 L8.432 38 L18.824 44 Z" fill="var(--splash-cell-fill)" opacity="0.55"/>
<path d="M50 62 L39.608 56 L39.608 68 L50 74 Z" fill="var(--splash-cell-fill)" opacity="0.35"/>
<path d="M39.608 56 L29.216 50 L29.216 62 L39.608 68 Z" fill="var(--splash-cell-fill)" opacity="0.1"/>
<path d="M29.216 50 L18.824 44 L18.824 56 L29.216 62 Z" fill="var(--splash-cell-fill)" opacity="0.5"/>
<path d="M18.824 44 L8.432 38 L8.432 50 L18.824 56 Z" fill="var(--splash-cell-fill)" opacity="0.25"/>
<path d="M50 74 L39.608 68 L39.608 80 L50 86 Z" fill="var(--splash-cell-fill)" opacity="0.4"/>
<path d="M39.608 68 L29.216 62 L29.216 74 L39.608 80 Z" fill="var(--splash-cell-fill)" opacity="0.3"/>
<path d="M29.216 62 L18.824 56 L18.824 68 L29.216 74 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
<path d="M18.824 56 L8.432 50 L8.432 62 L18.824 68 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
<path d="M50 86 L39.608 80 L39.608 92 L50 98 Z" fill="var(--splash-cell-fill)" opacity="0.55"/>
<path d="M39.608 80 L29.216 74 L29.216 86 L39.608 92 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M29.216 74 L18.824 68 L18.824 80 L29.216 86 Z" fill="var(--splash-cell-fill)" opacity="0.35"/>
<path d="M18.824 68 L8.432 62 L8.432 74 L18.824 80 Z" fill="var(--splash-cell-fill)" opacity="0.1"/>
<path d="M50 50 L91.568 26 L91.568 74 L50 98 Z" fill="var(--splash-face-fill)" stroke="var(--splash-stroke)" stroke-width="2" stroke-linejoin="round"/>
<path d="M50 50 L60.392 44 L60.392 56 L50 62 Z" fill="var(--splash-cell-fill)" opacity="0.3"/>
<path d="M60.392 44 L70.784 38 L70.784 50 L60.392 56 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
<path d="M70.784 38 L81.176 32 L81.176 44 L70.784 50 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
<path d="M81.176 32 L91.568 26 L91.568 38 L81.176 44 Z" fill="var(--splash-cell-fill)" opacity="0.25"/>
<path d="M50 62 L60.392 56 L60.392 68 L50 74 Z" fill="var(--splash-cell-fill)" opacity="0.5"/>
<path d="M60.392 56 L70.784 50 L70.784 62 L60.392 68 Z" fill="var(--splash-cell-fill)" opacity="0.35"/>
<path d="M70.784 50 L81.176 44 L81.176 56 L70.784 62 Z" fill="var(--splash-cell-fill)" opacity="0.1"/>
<path d="M81.176 44 L91.568 38 L91.568 50 L81.176 56 Z" fill="var(--splash-cell-fill)" opacity="0.4"/>
<path d="M50 74 L60.392 68 L60.392 80 L50 86 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M60.392 68 L70.784 62 L70.784 74 L60.392 80 Z" fill="var(--splash-cell-fill)" opacity="0.55"/>
<path d="M70.784 62 L81.176 56 L81.176 68 L70.784 74 Z" fill="var(--splash-cell-fill)" opacity="0.3"/>
<path d="M81.176 56 L91.568 50 L91.568 62 L81.176 68 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
<path d="M50 86 L60.392 80 L60.392 92 L50 98 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
<path d="M60.392 80 L70.784 74 L70.784 86 L60.392 92 Z" fill="var(--splash-cell-fill)" opacity="0.25"/>
<path d="M70.784 74 L81.176 68 L81.176 80 L70.784 86 Z" fill="var(--splash-cell-fill)" opacity="0.4"/>
<path d="M81.176 68 L91.568 62 L91.568 74 L81.176 80 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M50 2 L8.432 26 L50 50 L91.568 26 Z" fill="none" stroke="var(--splash-stroke)" stroke-width="2" stroke-linejoin="round"/>
<g transform="matrix(0.866, 0.5, -0.866, 0.5, 50, 26) scale(0.75)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-16 -20 L16 -20 L16 20 L-16 20 Z M-8 -12 L-8 12 L8 12 L8 -12 Z" fill="var(--splash-logo-fill)"/>
<path d="M-8 -4 L8 -4 L8 12 L-8 12 Z" fill="var(--splash-logo-fill)" fill-opacity="0.4"/>
</g>
</svg>
</body>
</html>
-26
View File
@@ -1,26 +0,0 @@
{
"name": "@openchamber/desktop",
"version": "1.12.0",
"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"
}
}
-135
View File
@@ -1,135 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '..', '..', '..');
const webDir = path.join(repoRoot, 'packages', 'web');
const desktopTauriDir = path.join(repoRoot, 'packages', 'desktop', 'src-tauri');
const resourcesDir = path.join(desktopTauriDir, 'resources');
const resourcesWebDistDir = path.join(resourcesDir, 'web-dist');
const webDistDir = path.join(webDir, 'dist');
const sidecarsDir = path.join(desktopTauriDir, 'sidecars');
const inferTargetTriple = () => {
if (typeof process.env.TAURI_ENV_TARGET_TRIPLE === 'string' && process.env.TAURI_ENV_TARGET_TRIPLE.trim()) {
return process.env.TAURI_ENV_TARGET_TRIPLE.trim();
}
if (process.platform === 'darwin') {
return process.arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin';
}
if (process.platform === 'win32') {
return 'x86_64-pc-windows-msvc';
}
if (process.platform === 'linux') {
return process.arch === 'arm64' ? 'aarch64-unknown-linux-gnu' : 'x86_64-unknown-linux-gnu';
}
return `${process.arch}-${process.platform}`;
};
const targetTriple = inferTargetTriple();
const bunCompileTargetByTriple = {
'aarch64-apple-darwin': 'bun-darwin-arm64',
'x86_64-apple-darwin': 'bun-darwin-x64',
'aarch64-unknown-linux-gnu': 'bun-linux-arm64',
'x86_64-unknown-linux-gnu': 'bun-linux-x64',
'x86_64-pc-windows-msvc': 'bun-windows-x64',
};
const compileTarget = bunCompileTargetByTriple[targetTriple];
if (!compileTarget) {
console.warn(
`[desktop] unknown target triple '${targetTriple}', falling back to host-arch sidecar build`
);
}
const sidecarBaseName = process.platform === 'win32'
? `openchamber-server-${targetTriple}.exe`
: `openchamber-server-${targetTriple}`;
const sidecarOutPath = path.join(sidecarsDir, sidecarBaseName);
const run = (cmd, args, cwd) => {
const result = spawnSync(cmd, args, { cwd, stdio: 'inherit' });
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`Command failed: ${cmd} ${args.join(' ')}`);
}
};
const resolveBun = () => {
if (typeof process.env.BUN === 'string' && process.env.BUN.trim()) {
return process.env.BUN.trim();
}
const result = spawnSync('/bin/bash', ['-lc', 'command -v bun'], { encoding: 'utf8' });
const resolved = (result.stdout || '').trim();
if (resolved) {
return resolved;
}
return 'bun';
};
const bunExe = resolveBun();
const copyDir = async (src, dst) => {
await fs.mkdir(dst, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const from = path.join(src, entry.name);
const to = path.join(dst, entry.name);
if (entry.isDirectory()) {
await copyDir(from, to);
} else if (entry.isSymbolicLink()) {
const link = await fs.readlink(from);
await fs.symlink(link, to);
} else {
await fs.copyFile(from, to);
}
}
};
console.log('[desktop] building web UI dist...');
run(bunExe, ['run', 'build'], webDir);
console.log('[desktop] preparing tauri resources...');
await fs.mkdir(resourcesDir, { recursive: true });
await fs.rm(resourcesWebDistDir, { recursive: true, force: true });
await copyDir(webDistDir, resourcesWebDistDir);
console.log('[desktop] building openchamber-server sidecar...');
await fs.mkdir(sidecarsDir, { recursive: true });
const buildArgs = [
'build',
'--compile',
path.join(webDir, 'server', 'index.js'),
'--outfile',
sidecarOutPath,
];
if (compileTarget) {
buildArgs.push('--target', compileTarget);
}
run(bunExe, buildArgs, repoRoot);
if (process.platform !== 'win32') {
await fs.chmod(sidecarOutPath, 0o755);
}
console.log(`[desktop] sidecar ready: ${sidecarOutPath}`);
console.log(`[desktop] web assets ready: ${resourcesWebDistDir}`);
-138
View File
@@ -1,138 +0,0 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '../../..');
const desktopDir = path.join(repoRoot, 'packages/desktop');
function spawnProcess(command, args, opts = {}) {
return spawn(command, args, {
cwd: repoRoot,
env: { ...process.env },
stdio: 'inherit',
detached: process.platform !== 'win32',
...opts,
});
}
function waitForExit(child, timeoutMs) {
return new Promise((resolve) => {
if (!child || child.exitCode !== null || child.signalCode !== null) {
resolve();
return;
}
const onExit = () => {
clearTimeout(timer);
resolve();
};
const timer = setTimeout(() => {
child.off('exit', onExit);
resolve();
}, timeoutMs);
child.once('exit', onExit);
});
}
function signalChild(child, signal) {
if (!child || child.exitCode !== null || child.signalCode !== null) {
return;
}
try {
if (process.platform !== 'win32') {
process.kill(-child.pid, signal);
return;
}
} catch {
}
try {
child.kill(signal);
} catch {
}
}
async function stopChildTree(child) {
if (!child || child.exitCode !== null || child.signalCode !== null) {
return;
}
signalChild(child, 'SIGINT');
await waitForExit(child, 2500);
if (child.exitCode === null && child.signalCode === null) {
signalChild(child, 'SIGTERM');
await waitForExit(child, 2500);
}
if (child.exitCode === null && child.signalCode === null) {
signalChild(child, 'SIGKILL');
await waitForExit(child, 1000);
}
}
async function main() {
const tauriProcess = spawnProcess('bun', [
'--cwd',
desktopDir,
'tauri',
'dev',
'--features',
'devtools',
'--config',
'./src-tauri/tauri.dev.conf.json',
]);
let cleaning = false;
const teardown = async (code) => {
if (cleaning) {
return;
}
cleaning = true;
await stopChildTree(tauriProcess);
process.exit(typeof code === 'number' ? code : 0);
};
const handleChildExit = (childName) => (code, signal) => {
if (code !== 0 || signal) {
console.warn(`[desktop:dev] ${childName} exited with code ${code ?? 'null'} signal ${signal ?? 'none'}.`);
}
teardown(code).catch((error) => {
console.error('[desktop:dev] Cleanup error:', error);
process.exit(code ?? 1);
});
};
tauriProcess.on('exit', handleChildExit('Tauri dev process'));
const errorHandler = (label) => (error) => {
console.error(`[desktop:dev] Failed to start ${label}:`, error);
teardown(1).catch(() => process.exit(1));
};
tauriProcess.on('error', errorHandler('Tauri dev process'));
const signalExitCodes = {
SIGINT: 130,
SIGTERM: 143,
SIGQUIT: 131,
};
Object.entries(signalExitCodes).forEach(([signal, exitCode]) => {
process.on(signal, () => {
teardown(exitCode).catch(() => process.exit(exitCode));
});
});
}
main().catch((error) => {
console.error('[desktop:dev] Unexpected error:', error);
process.exit(1);
});
-197
View File
@@ -1,197 +0,0 @@
import path from 'node:path';
import { spawn, spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const DESKTOP_DEV_PORT = 3901;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '..', '..', '..');
const desktopDir = path.join(repoRoot, 'packages', 'desktop');
const tauriDir = path.join(desktopDir, 'src-tauri');
const inferTargetTriple = () => {
const fromEnv = typeof process.env.TAURI_ENV_TARGET_TRIPLE === 'string' ? process.env.TAURI_ENV_TARGET_TRIPLE.trim() : '';
if (fromEnv) return fromEnv;
if (process.platform === 'darwin') {
return process.arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin';
}
if (process.platform === 'win32') {
return 'x86_64-pc-windows-msvc';
}
if (process.platform === 'linux') {
return process.arch === 'arm64' ? 'aarch64-unknown-linux-gnu' : 'x86_64-unknown-linux-gnu';
}
return `${process.arch}-${process.platform}`;
};
const targetTriple = inferTargetTriple();
const sidecarName = process.platform === 'win32'
? `openchamber-server-${targetTriple}.exe`
: `openchamber-server-${targetTriple}`;
const sidecarPath = path.join(tauriDir, 'sidecars', sidecarName);
const distDir = path.join(tauriDir, 'resources', 'web-dist');
const webDir = path.join(repoRoot, 'packages', 'web');
const run = (cmd, args, cwd) => {
const result = spawnSync(cmd, args, { cwd, stdio: 'inherit' });
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`Command failed: ${cmd} ${args.join(' ')}`);
}
};
console.log('[desktop] ensuring sidecar + web-dist...');
run('node', ['./scripts/build-sidecar.mjs'], desktopDir);
console.log(`[desktop] starting API server on http://127.0.0.1:${DESKTOP_DEV_PORT} ...`);
const apiChild = spawn(sidecarPath, ['--port', String(DESKTOP_DEV_PORT)], {
cwd: repoRoot,
stdio: 'inherit',
detached: process.platform !== 'win32',
env: {
...process.env,
OPENCHAMBER_HOST: '127.0.0.1',
OPENCHAMBER_DIST_DIR: distDir,
NO_PROXY: process.env.NO_PROXY || 'localhost,127.0.0.1',
no_proxy: process.env.no_proxy || 'localhost,127.0.0.1',
},
});
console.log('[desktop] starting Vite HMR server on http://127.0.0.1:5173 ...');
const webChild = spawn('bun', ['x', 'vite', '--host', '127.0.0.1', '--port', '5173', '--strictPort'], {
cwd: webDir,
stdio: 'inherit',
detached: process.platform !== 'win32',
env: {
...process.env,
OPENCHAMBER_PORT: String(DESKTOP_DEV_PORT),
NO_PROXY: process.env.NO_PROXY || 'localhost,127.0.0.1',
no_proxy: process.env.no_proxy || 'localhost,127.0.0.1',
},
});
let shuttingDown = false;
function waitForExit(child, timeoutMs) {
return new Promise((resolve) => {
if (!child || child.exitCode !== null || child.signalCode !== null) {
resolve();
return;
}
const onExit = () => {
clearTimeout(timer);
resolve();
};
const timer = setTimeout(() => {
child.off('exit', onExit);
resolve();
}, timeoutMs);
child.once('exit', onExit);
});
}
function signalChild(child, signal) {
if (!child || child.exitCode !== null || child.signalCode !== null) {
return;
}
try {
if (process.platform !== 'win32') {
process.kill(-child.pid, signal);
return;
}
} catch {
}
try {
child.kill(signal);
} catch {
}
}
async function requestApiShutdown() {
const url = `http://127.0.0.1:${DESKTOP_DEV_PORT}/api/system/shutdown`;
try {
await fetch(url, { method: 'POST' });
} catch {
}
}
async function stopChildTree(child) {
if (!child || child.exitCode !== null || child.signalCode !== null) {
return;
}
signalChild(child, 'SIGINT');
await waitForExit(child, 2500);
if (child.exitCode === null && child.signalCode === null) {
signalChild(child, 'SIGTERM');
await waitForExit(child, 2500);
}
if (child.exitCode === null && child.signalCode === null) {
signalChild(child, 'SIGKILL');
await waitForExit(child, 1000);
}
}
const shutdown = async (exitCode = 0) => {
if (shuttingDown) return;
shuttingDown = true;
await requestApiShutdown();
await Promise.all([stopChildTree(webChild), stopChildTree(apiChild)]);
process.exit(exitCode);
};
const handleExit = (label) => (code, signal) => {
if (shuttingDown) {
return;
}
if (code !== 0 || signal) {
console.error(`[desktop] ${label} exited unexpectedly (code=${code ?? 'null'} signal=${signal ?? 'none'})`);
}
shutdown(typeof code === 'number' ? code : 1).catch((error) => {
console.error('[desktop] shutdown failed:', error);
process.exit(1);
});
};
apiChild.on('exit', handleExit('API server'));
webChild.on('exit', handleExit('Vite server'));
const handleError = (label) => (error) => {
if (shuttingDown) {
return;
}
console.error(`[desktop] failed to start ${label}:`, error);
shutdown(1).catch(() => process.exit(1));
};
apiChild.on('error', handleError('API server'));
webChild.on('error', handleError('Vite server'));
process.on('SIGINT', () => {
shutdown(130).catch(() => process.exit(130));
});
process.on('SIGTERM', () => {
shutdown(143).catch(() => process.exit(143));
});
process.on('SIGHUP', () => {
shutdown(129).catch(() => process.exit(129));
});
-237
View File
@@ -1,237 +0,0 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { access, readFile, unlink, writeFile } from 'node:fs/promises';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const desktopDir = path.resolve(__dirname, '..');
const stateFile = path.join(desktopDir, '.opencode-cli-state.json');
const DEFAULT_BIN_CANDIDATES = [
process.env.OPENCHAMBER_OPENCODE_PATH,
process.env.OPENCHAMBER_OPENCODE_BIN,
process.env.OPENCODE_PATH,
process.env.OPENCODE_BINARY,
'/opt/homebrew/bin/opencode',
'/usr/local/bin/opencode',
'/usr/bin/opencode',
path.join(os.homedir(), '.local/bin/opencode'),
].filter(Boolean);
const CLI_ARGS_ENV = process.env.OPENCHAMBER_OPENCODE_ARGS;
const DEFAULT_ARGS = CLI_ARGS_ENV
? parseArgs(CLI_ARGS_ENV)
: ['api'];
function parseArgs(raw) {
if (!raw || typeof raw !== 'string') {
return [];
}
const trimmed = raw.trim();
if (!trimmed) {
return [];
}
if (trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed);
if (Array.isArray(parsed) && parsed.every((item) => typeof item === 'string')) {
return parsed;
}
} catch {
// fall through to whitespace split
}
}
return trimmed.split(/\s+/g);
}
async function fileExists(targetPath) {
try {
await access(targetPath, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
async function resolveCliPath() {
for (const candidate of DEFAULT_BIN_CANDIDATES) {
if (candidate && await fileExists(candidate)) {
return candidate;
}
}
const envPath = process.env.PATH || '';
for (const segment of envPath.split(path.delimiter)) {
const candidate = path.join(segment, 'opencode');
if (await fileExists(candidate)) {
return candidate;
}
}
throw new Error('Unable to locate the OpenCode CLI. Set OPENCHAMBER_OPENCODE_PATH to the executable.');
}
async function readState() {
try {
const raw = await readFile(stateFile, 'utf8');
const data = JSON.parse(raw);
if (typeof data?.pid === 'number') {
return data;
}
} catch {
// ignore
}
return null;
}
function isProcessAlive(pid) {
if (!pid || typeof pid !== 'number') {
return false;
}
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
async function writeState(pid) {
await writeFile(stateFile, JSON.stringify({ pid }), 'utf8');
}
async function removeStateFile() {
try {
await unlink(stateFile);
} catch {
// already removed
}
}
function spawnCli(cliPath, args) {
const env = {
...process.env,
OPENCHAMBER_OPENCODE_PORT: process.env.OPENCHAMBER_OPENCODE_PORT || process.env.OPENCODE_PORT || process.env.OPENCHAMBER_INTERNAL_PORT || '0',
};
const cwd = process.env.OPENCHAMBER_OPENCODE_CWD || process.cwd();
const child = spawn(cliPath, args.length > 0 ? args : DEFAULT_ARGS, {
cwd,
env,
detached: true,
stdio: 'ignore',
});
child.unref();
return child;
}
export async function startCli({ silent = false } = {}) {
const existing = await readState();
if (existing?.pid && isProcessAlive(existing.pid)) {
if (!silent) {
console.log(`[desktop:start-cli] OpenCode CLI already running (pid ${existing.pid}).`);
}
return existing.pid;
}
const cliPath = await resolveCliPath();
const child = spawnCli(cliPath, DEFAULT_ARGS);
await writeState(child.pid);
if (!silent) {
console.log(`[desktop:start-cli] OpenCode CLI started (${cliPath}) pid ${child.pid}.`);
}
return child.pid;
}
export async function stopCli({ silent = false } = {}) {
const state = await readState();
if (!state?.pid) {
if (!silent) {
console.log('[desktop:stop-cli] No OpenCode CLI PID recorded.');
}
return;
}
const { pid } = state;
if (!isProcessAlive(pid)) {
await removeStateFile();
if (!silent) {
console.log('[desktop:stop-cli] CLI already stopped.');
}
return;
}
try {
process.kill(pid, 'SIGTERM');
} catch (error) {
if (!silent) {
console.error(`[desktop:stop-cli] Failed to send SIGTERM to pid ${pid}:`, error);
}
}
const timeoutMs = 5000;
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (!isProcessAlive(pid)) {
await removeStateFile();
if (!silent) {
console.log('[desktop:stop-cli] OpenCode CLI stopped.');
}
return;
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
try {
process.kill(pid, 'SIGKILL');
if (!silent) {
console.warn(`[desktop:stop-cli] Forced termination sent to pid ${pid}.`);
}
} catch (error) {
if (!silent) {
console.error(`[desktop:stop-cli] Unable to terminate pid ${pid}:`, error);
}
} finally {
await removeStateFile();
}
}
async function main() {
const [, , command] = process.argv;
if (!command || command === '--help' || command === '-h') {
console.log('Usage: node opencode-cli.mjs <start|stop|status>');
process.exit(0);
}
if (command === 'start') {
await startCli();
return;
}
if (command === 'stop') {
await stopCli();
return;
}
if (command === 'status') {
const state = await readState();
if (state?.pid && isProcessAlive(state.pid)) {
console.log(`OpenCode CLI running (pid ${state.pid}).`);
} else {
console.log('OpenCode CLI not running.');
}
process.exit(0);
return;
}
console.error(`Unknown command: ${command}`);
process.exit(1);
}
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
main().catch((error) => {
console.error('[desktop:opencode-cli] Unexpected error:', error);
process.exit(1);
});
}
File diff suppressed because it is too large Load Diff
-42
View File
@@ -1,42 +0,0 @@
[package]
name = "openchamber-desktop"
version = "1.12.0"
edition = "2021"
publish = false
[[bin]]
name = "openchamber-desktop"
path = "src/main.rs"
[features]
default = []
devtools = ["tauri/devtools"]
[dependencies]
anyhow = "1.0.86"
base64 = "0.22.1"
log = "0.4.28"
reqwest = { version = "0.12.4", default-features = false, features = ["rustls-tls", "json"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.143"
tauri = { version = "2.10.3", features = ["macos-private-api"] }
tauri-plugin-dialog = "2.6.0"
tauri-plugin-log = "2.8.0"
tauri-plugin-shell = "2.3.5"
tauri-plugin-notification = "2.3.3"
tauri-plugin-updater = "2.10.0"
tokio = { version = "1.38", features = ["rt-multi-thread", "time", "macros", "sync"] }
url = "2.5"
[build-dependencies]
tauri-build = { version = "2.5.6", features = [] }
[profile.release]
lto = "thin"
codegen-units = 1
strip = true
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-web-kit = "0.3"
rfd = "0.15"
-27
View File
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSSupportsAutomaticTermination</key>
<false/>
<key>NSSupportsSuddenTermination</key>
<false/>
<key>NSAppleEventsUsageDescription</key>
<string>OpenChamber needs to run the OpenCode CLI to provide AI coding assistance.</string>
<key>NSDesktopFolderUsageDescription</key>
<string>OpenChamber needs access to work with your projects.</string>
<key>NSDocumentsFolderUsageDescription</key>
<string>OpenChamber needs access to work with your projects.</string>
<key>NSDownloadsFolderUsageDescription</key>
<string>OpenChamber needs access to work with your projects.</string>
<key>NSMicrophoneUsageDescription</key>
<string>OpenChamber needs microphone access for voice input.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>OpenChamber needs speech recognition to transcribe voice input.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
</dict>
</plist>
-3
View File
@@ -1,3 +0,0 @@
fn main() {
tauri_build::build();
}
@@ -1,41 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capabilities for OpenChamber desktop runtime",
"remote": {
"urls": [
"http://127.0.0.1:*/*",
"http://localhost:*/*",
"http://*",
"http://*/*",
"https://*",
"https://*/*"
]
},
"windows": ["main", "main-*"],
"permissions": [
"core:default",
"core:window:default",
"core:window:allow-close",
"core:window:allow-set-title",
"core:window:allow-set-size",
"core:window:allow-set-position",
"core:window:allow-start-dragging",
"core:webview:default",
"core:webview:allow-webview-close",
"shell:allow-open",
"shell:allow-execute",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
"notification:default",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:allow-notify",
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install"
]
}
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!--
Intentionally NOT sandboxed. This app is distributed outside the Mac App Store.
Do not add com.apple.security.app-sandbox.
These entitlements are commonly required for WKWebView/WebKit JIT behavior
under hardened runtime.
-->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

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

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,82 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"productName": "OpenChamber",
"version": "1.12.0",
"identifier": "ai.opencode.openchamber",
"build": {
"beforeDevCommand": "node ./scripts/dev-web-server.mjs",
"beforeBuildCommand": "bun run build:sidecar",
"devUrl": "http://127.0.0.1:3901",
"frontendDist": "../noop-dist"
},
"app": {
"windows": [
{
"label": "main",
"create": false,
"title": "OpenChamber",
"width": 1280,
"height": 800,
"resizable": true,
"fullscreen": false,
"decorations": true,
"hiddenTitle": true,
"titleBarStyle": "Overlay",
"trafficLightPosition": {
"x": 17,
"y": 26
},
"dragDropEnabled": false,
"visible": false
}
],
"security": {
"csp": null
},
"withGlobalTauri": true,
"macOSPrivateApi": true
},
"bundle": {
"active": true,
"externalBin": [
"sidecars/openchamber-server"
],
"resources": [
"resources/web-dist/**/*"
],
"icon": [
"icons/icon.icns",
"icons/icon.png"
],
"macOS": {
"exceptionDomain": "localhost",
"minimumSystemVersion": "13.0",
"signingIdentity": null,
"entitlements": "./entitlements.plist",
"infoPlist": "Info.plist",
"dmg": {
"appPosition": {
"x": 180,
"y": 170
},
"applicationFolderPosition": {
"x": 480,
"y": 170
},
"windowSize": {
"width": 660,
"height": 400
}
}
},
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"endpoints": [
"https://github.com/btriapitsyn/openchamber/releases/latest/download/latest.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwRDMyQUQzNjNFRTc1ODIKUldTQ2RlNWoweXJUSUdpVWVWNm84R1pHamYzNVFhYWgyWmlpWFVzem5nUTlHd1dlRlNTV0FFc3IK"
}
}
}
@@ -1,9 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"bundle": {
"icon": [
"icons/dev-icon.icns",
"icons/dev-icon.png"
]
}
}
+5 -17
View File
@@ -152,7 +152,6 @@ const LOCAL_DESKTOP_CLIENT_KIND = 'desktop-local';
const LOCAL_DESKTOP_CLIENT_DEDUPE_KEY = 'desktop-local';
const ENV_OVERRIDE_HOST_ID = '__env';
const CHANGELOG_URL = 'https://raw.githubusercontent.com/openchamber/openchamber/main/CHANGELOG.md';
const UPDATE_METADATA_URL = 'https://github.com/openchamber/openchamber/releases/latest/download/latest.json';
const GITHUB_BUG_REPORT_URL = 'https://github.com/openchamber/openchamber/issues/new?template=bug_report.yml';
const GITHUB_FEATURE_REQUEST_URL = 'https://github.com/openchamber/openchamber/issues/new?template=feature_request.yml';
const DISCORD_INVITE_URL = 'https://discord.gg/ZYRSdnwwKA';
@@ -1681,7 +1680,6 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
backgroundColor: '#151313',
frame: process.platform === 'win32' ? false : undefined,
autoHideMenuBar: autoHidesNativeMenuBar,
// Tauri used an overlay title bar with explicit traffic-light placement.
// Electron's hiddenInset adds its own extra inset, which leaves the controls
// visibly lower than the app header. Use a plain hidden title bar instead.
titleBarStyle: usesCustomTitleBar ? 'hidden' : 'default',
@@ -1704,7 +1702,7 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
// sandbox must stay off: the preload uses contextBridge + ipcRenderer
// from Electron's Node layer. contextIsolation + nodeIntegration:false
// keep the renderer world walled off from Node. Do NOT flip to true —
// the preload would fail to load and __TAURI__ would go undefined.
// the preload would fail to load and the desktop bridge would be unavailable.
sandbox: false,
},
};
@@ -3028,9 +3026,8 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
}
case 'desktop_set_vibrancy': {
// Vibrancy (macOS blur) is not supported in the Electron shell — the
// Tauri build used NSVisualEffectView via Tauri plugin, Electron has
// no equivalent for our titleBarStyle:'hidden' setup. Persist the
// Vibrancy (macOS blur) is not supported in the Electron shell for our
// titleBarStyle:'hidden' setup. Persist the
// disabled state so settings UI reflects it; args.enabled is ignored.
await mutateSettingsRoot((root) => {
root.desktopVibrancy = false;
@@ -3040,13 +3037,6 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
case 'desktop_check_for_updates': {
const currentVersion = APP_VERSION;
let payload = null;
try {
const response = await fetch(UPDATE_METADATA_URL, { signal: AbortSignal.timeout(10_000) });
payload = await response.json();
} catch {
}
let updateResult = null;
try {
updateResult = await autoUpdater.checkForUpdates();
@@ -3056,14 +3046,12 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
const updateInfo = updateResult?.updateInfo;
const nextVersion =
(typeof updateInfo?.version === 'string' && updateInfo.version) ||
(typeof payload?.version === 'string' && payload.version) ||
currentVersion;
const available = compareSemver(nextVersion, currentVersion) > 0;
const body =
(typeof payload?.notes === 'string' && payload.notes.trim() ? payload.notes : null) ||
(typeof updateInfo?.releaseNotes === 'string' && updateInfo.releaseNotes.trim() ? updateInfo.releaseNotes : null) ||
await parseRelevantChangelogNotes(currentVersion, nextVersion);
state.pendingUpdate = available ? { version: nextVersion, metadata: payload, electronUpdate: updateResult } : null;
state.pendingUpdate = available ? { version: nextVersion, electronUpdate: updateResult } : null;
return {
available,
currentVersion,
@@ -3071,7 +3059,7 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
body: body || null,
date:
(typeof updateInfo?.releaseDate === 'string' && updateInfo.releaseDate) ||
(typeof payload?.pub_date === 'string' ? payload.pub_date : null),
null,
};
}
+8 -16
View File
@@ -24,8 +24,8 @@ const macosMajor = Number.parseInt(macosMajorRaw, 10);
// Remote UIs still need it so isDesktopShell() returns true and the
// window renders with desktop affordances (DesktopHostSwitcher,
// title bar offsets, etc.). Expose unconditionally.
// - __TAURI__ is the IPC channel to the main process. The compatibility
// shim is exposed broadly, but privileged commands are gated in main.mjs.
// - __OPENCHAMBER_DESKTOP__ is the IPC channel to the main process. It is
// exposed broadly, but privileged commands are gated in main.mjs.
// Local-only globals below stay limited to packaged UI / exact localOrigin.
// Everything driven by localOrigin (home dir, macOS hints) also stays
// local-only since it leaks info about the Electron host machine.
@@ -136,21 +136,13 @@ ipcRenderer.on('openchamber:emit', (_evt, payload) => {
dispatchNativeEvent(event, payload.detail);
});
// __TAURI__ is exposed on all pages; the main-process gate in
// The desktop bridge is exposed on all pages; the main-process gate in
// ipcMain.handle('openchamber:invoke') decides per-command what is safe
// for non-local callers (window/host-switcher ops yes, file/shell ops
// no). See COMMANDS_SAFE_FOR_REMOTE in main.mjs.
contextBridge.exposeInMainWorld('__TAURI__', {
core: {
invoke: (cmd, args) => ipcRenderer.invoke('openchamber:invoke', cmd, args || {}),
},
dialog: {
open: (options) => ipcRenderer.invoke('openchamber:dialog:open', options || {}),
},
shell: {
open: (url) => ipcRenderer.invoke('openchamber:invoke', 'desktop_open_external_url', { url }),
},
event: {
listen: async (event, handler) => addListener(event, handler),
},
contextBridge.exposeInMainWorld('__OPENCHAMBER_DESKTOP__', {
invoke: (cmd, args) => ipcRenderer.invoke('openchamber:invoke', cmd, args || {}),
openDialog: (options) => ipcRenderer.invoke('openchamber:dialog:open', options || {}),
openExternal: (url) => ipcRenderer.invoke('openchamber:invoke', 'desktop_open_external_url', { url }),
listen: async (event, handler) => addListener(event, handler),
});
-1
View File
@@ -83,7 +83,6 @@
"devDependencies": {
"@eslint/js": "^9.33.0",
"@tailwindcss/postcss": "^4.0.0",
"@tauri-apps/api": "^2.10.1",
"@types/node": "^24.3.1",
"@types/prismjs": "^1.26.6",
"@types/qrcode": "^1.5.5",
+2 -2
View File
@@ -17,7 +17,7 @@ import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt';
import { useWindowTitle } from '@/hooks/useWindowTitle';
import { useConfigStore } from '@/stores/useConfigStore';
import { hasModifier } from '@/lib/utils';
import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell, restartDesktopApp } from '@/lib/desktop';
import { isDesktopLocalOriginActive, isDesktopShell, restartDesktopApp } from '@/lib/desktop';
import {
getInjectedBootOutcome,
getBootInjectionStatus,
@@ -758,7 +758,7 @@ function App({ apis }: AppProps) {
const handleDesktopBootDismiss = React.useCallback(async () => {
if (shouldRestartDesktopBootFlow({
isTauriShell: isTauriShell(),
isDesktopShell: isDesktopShell(),
isDesktopLocalOriginActive: isDesktopLocalOriginActive(),
})) {
await restartDesktopApp();
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { toast } from '@/components/ui';
import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
import { invokeDesktop, isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
import { syncDesktopSettings, initializeAppearancePreferences } from '@/lib/persistence';
import { applyPersistedDirectoryPreferences } from '@/lib/directoryPersistence';
import { DesktopHostSwitcherInline } from '@/components/desktop/DesktopHostSwitcher';
@@ -129,11 +129,7 @@ const issueDesktopClientTokenViaShell = async (password: string, trustDevice: bo
if (!isDesktopShell() || typeof window === 'undefined') {
return '';
}
const invoke = (window as unknown as { __TAURI__?: { core?: { invoke?: (cmd: string, args?: Record<string, unknown>) => Promise<unknown> } } }).__TAURI__?.core?.invoke;
if (typeof invoke !== 'function') {
return '';
}
const response = await invoke('desktop_remote_password_login', {
const response = await invokeDesktop('desktop_remote_password_login', {
url: getRuntimeApiBaseUrl(),
password,
trustDevice,
+1 -118
View File
@@ -36,8 +36,7 @@ import { useCurrentSessionActivity } from '@/hooks/useSessionActivity';
import { toast } from '@/components/ui';
import { Button } from '@/components/ui/button';
// useMessageStore removed — messages now come from sync system
import { isTauriShell, isVSCodeRuntime } from '@/lib/desktop';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { isVSCodeRuntime } from '@/lib/desktop';
import { isIMECompositionEvent } from '@/lib/ime';
import { StopIcon } from '@/components/icons/StopIcon';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
@@ -968,7 +967,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
const suppressNextFileDropTextInsertTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const pendingDroppedAbsolutePathsRef = React.useRef<string[]>([]);
const canAcceptDropRef = React.useRef(false);
const nativeDragInsideDropZoneRef = React.useRef(false);
const mentionRef = React.useRef<FileMentionHandle>(null);
const commandRef = React.useRef<CommandAutocompleteHandle>(null);
const skillRef = React.useRef<SkillAutocompleteHandle>(null);
@@ -3407,121 +3405,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
}
};
// Tauri desktop: handle native file drops via onDragDropEvent
React.useEffect(() => {
if (!isTauriShell()) return;
let cancelled = false;
let unlisten: (() => void) | null = null;
void (async () => {
try {
const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow');
const webviewWindow = getCurrentWebviewWindow();
const removeListener = await webviewWindow.onDragDropEvent(async (event) => {
if (!canAcceptDropRef.current) return;
const payload = (event as { payload?: unknown }).payload;
if (!payload || typeof payload !== 'object') return;
const typed = payload as { type?: string; paths?: string[]; position?: { x?: number; y?: number } };
const type = typed.type;
const x = typed.position?.x;
const y = typed.position?.y;
// Check if drop is inside the chat input area
const zone = dropZoneRef.current;
let inZone: boolean | null = null;
if (zone && typeof x === 'number' && typeof y === 'number') {
const rect = zone.getBoundingClientRect();
inZone = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
// Handle retina displays where Tauri might report physical pixels
if (!inZone && window.devicePixelRatio > 1) {
const sx = x / window.devicePixelRatio;
const sy = y / window.devicePixelRatio;
inZone = sx >= rect.left && sx <= rect.right && sy >= rect.top && sy <= rect.bottom;
}
}
if (type === 'enter' || type === 'over') {
if (inZone !== null) {
nativeDragInsideDropZoneRef.current = inZone;
}
setIsDragging(nativeDragInsideDropZoneRef.current);
return;
}
if (type === 'leave') {
nativeDragInsideDropZoneRef.current = false;
setIsDragging(false);
return;
}
if (type === 'drop') {
const shouldHandleDrop = inZone ?? nativeDragInsideDropZoneRef.current;
nativeDragInsideDropZoneRef.current = false;
setIsDragging(false);
if (!shouldHandleDrop) return;
const paths = Array.isArray(typed.paths)
? typed.paths.filter((p): p is string => typeof p === 'string')
: [];
if (paths.length === 0) return;
for (const path of paths) {
try {
const normalizedPath = normalizeDroppedPath(path);
const fileName = normalizedPath.split(/[\\/]/).pop() || normalizedPath;
let file: File;
// In Tauri shell, dropped paths are local machine paths.
// Read bytes via native command to avoid workspace-bound /api/fs/raw restrictions.
if (isTauriShell()) {
const { invoke } = await import('@tauri-apps/api/core');
const result = await invoke<{ mime: string; base64: string }>('desktop_read_file', { path: normalizedPath });
const byteCharacters = atob(result.base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: result.mime || 'application/octet-stream' });
file = new File([blob], fileName, { type: result.mime || 'application/octet-stream' });
} else {
const response = await runtimeFetch('/api/fs/raw', { query: { path: normalizedPath } });
if (!response.ok) {
throw new Error(`Failed to read dropped file (${response.status})`);
}
const blob = await response.blob();
file = new File([blob], fileName, { type: blob.type || 'application/octet-stream' });
}
await addAttachedFile(file);
} catch (error) {
console.error('Failed to attach dropped file:', path, error);
toast.error(t('chat.chatInput.toast.attachNamedFailed', {
name: path.split(/[\\/]/).pop() || t('chat.chatInput.fileFallback'),
}));
}
}
}
});
if (cancelled) {
removeListener();
return;
}
unlisten = removeListener;
} catch (error) {
if (!cancelled) {
console.warn('Failed to register Tauri drag-drop listener:', error);
}
}
})();
return () => {
cancelled = true;
if (unlisten) unlisten();
};
}, [addAttachedFile, normalizeDroppedPath, t]);
const fileInputRef = React.useRef<HTMLInputElement>(null);
const attachFiles = React.useCallback(async (files: FileList | File[]) => {
@@ -11,7 +11,7 @@ import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { toast } from '@/components/ui';
import { isElectronShell, isTauriShell, isDesktopShell } from '@/lib/desktop';
import { isElectronShell, isDesktopShell } from '@/lib/desktop';
import { Icon } from "@/components/icon/Icon";
import { useUIStore } from '@/stores/useUIStore';
import { useI18n } from '@/lib/i18n';
@@ -354,7 +354,7 @@ export function DesktopHostSwitcherDialog({
}, [allHosts, defaultHostId, t]);
const persist = React.useCallback(async (nextHosts: DesktopHost[], nextDefaultHostId: string | null) => {
if (!isTauriShell()) return;
if (!isDesktopShell()) return;
setIsSaving(true);
setError('');
try {
@@ -376,7 +376,7 @@ export function DesktopHostSwitcherDialog({
}, [onOpenChange, setSettingsDialogOpen, setSettingsPage]);
const refresh = React.useCallback(async () => {
if (!isTauriShell()) return;
if (!isDesktopShell()) return;
setIsLoading(true);
setError('');
try {
@@ -408,7 +408,7 @@ export function DesktopHostSwitcherDialog({
}, [t]);
const probeAll = React.useCallback(async (hosts: DesktopHost[]) => {
if (!isTauriShell()) return;
if (!isDesktopShell()) return;
setIsProbing(true);
const nextProbingHostIds: Record<string, true> = {};
for (const host of hosts) {
@@ -458,7 +458,7 @@ export function DesktopHostSwitcherDialog({
}, [open, allHosts, probeAll]);
React.useEffect(() => {
if (!open || !isTauriShell()) {
if (!open || !isDesktopShell()) {
return;
}
let cancelled = false;
@@ -512,7 +512,7 @@ export function DesktopHostSwitcherDialog({
const isSshHost = Boolean(sshHostIds[host.id]);
if (host.id !== LOCAL_HOST_ID && isSshHost && isTauriShell()) {
if (host.id !== LOCAL_HOST_ID && isSshHost && isDesktopShell()) {
let existingStatus = sshStatusesById[host.id];
const latestStatus = await desktopSshStatus(host.id)
.then((items) => items.find((item) => item.id === host.id) || null)
@@ -596,7 +596,7 @@ export function DesktopHostSwitcherDialog({
}
}
if (host.id !== LOCAL_HOST_ID && isTauriShell()) {
if (host.id !== LOCAL_HOST_ID && isDesktopShell()) {
setSwitchingHostId(host.id);
const probe = await desktopHostProbe(origin, { clientToken: host.clientToken || null }).catch((): HostProbeResult => ({ status: 'unreachable', latencyMs: 0 }));
setStatusById((prev) => ({
@@ -705,7 +705,7 @@ export function DesktopHostSwitcherDialog({
error: null,
});
if (!hostId || hostId === LOCAL_HOST_ID || !isTauriShell()) {
if (!hostId || hostId === LOCAL_HOST_ID || !isDesktopShell()) {
return;
}
@@ -721,7 +721,7 @@ export function DesktopHostSwitcherDialog({
}, [allHosts, handleSwitch, sshSwitchModal.hostId]);
const connectSshHostInPlace = React.useCallback(async (host: DesktopHost) => {
if (!isTauriShell()) return;
if (!isDesktopShell()) return;
setSwitchingHostId(host.id);
try {
await desktopSshConnect(host.id);
@@ -750,7 +750,7 @@ export function DesktopHostSwitcherDialog({
return null;
}
const tauriAvailable = isTauriShell();
const desktopAvailable = isDesktopShell();
const content = (
<>
@@ -772,7 +772,7 @@ export function DesktopHostSwitcherDialog({
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'
)}
onClick={() => void probeAll(allHosts)}
disabled={!tauriAvailable || isLoading || isProbing}
disabled={!desktopAvailable || isLoading || isProbing}
aria-label={t('desktopHostSwitcher.actions.refreshInstancesAria')}
>
<Icon name="refresh" className={cn('h-4 w-4', isProbing && 'animate-spin')} />
@@ -805,7 +805,7 @@ export function DesktopHostSwitcherDialog({
size="sm"
variant="ghost"
onClick={() => void probeAll(allHosts)}
disabled={!tauriAvailable || isLoading || isProbing}
disabled={!desktopAvailable || isLoading || isProbing}
>
<Icon name="refresh" className={cn('h-4 w-4', isProbing && 'animate-spin')} />
{t('desktopHostSwitcher.actions.refresh')}
@@ -814,7 +814,7 @@ export function DesktopHostSwitcherDialog({
</div>
)}
{!tauriAvailable && (
{!desktopAvailable && (
<div className="flex-shrink-0 rounded-lg border border-border/50 bg-muted/20 p-3">
<div className="typography-meta text-muted-foreground">
{t('desktopHostSwitcher.state.limitedOnPage')}
@@ -974,7 +974,7 @@ export function DesktopHostSwitcherDialog({
</div>
</div>
{tauriAvailable && editingId && editingId !== LOCAL_HOST_ID && (
{desktopAvailable && editingId && editingId !== LOCAL_HOST_ID && (
<div className="flex-shrink-0 rounded-lg border border-border/50 bg-muted/20 p-3">
<div className="flex items-center justify-between gap-2">
<div className="typography-ui-label font-medium text-foreground">{t('desktopHostSwitcher.edit.title')}</div>
@@ -1202,7 +1202,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost
}, [connectDefaultSshInstance, startupSshModal.hostId, startupSshModal.hostLabel]);
React.useEffect(() => {
if (!isTauriShell()) return;
if (!isDesktopShell()) return;
let cancelled = false;
const run = async () => {
@@ -10,7 +10,7 @@ import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { copyTextToClipboard } from '@/lib/clipboard';
import { cn } from '@/lib/utils';
import { isDesktopLocalOriginActive, isTauriShell, openDesktopPath, openDesktopProjectInApp } from '@/lib/desktop';
import { isDesktopLocalOriginActive, isDesktopShell, openDesktopPath, openDesktopProjectInApp } from '@/lib/desktop';
import { DEFAULT_OPEN_IN_APP_ID, OPEN_IN_APPS } from '@/lib/openInApps';
import { useOpenInAppsStore, type OpenInAppOption } from '@/stores/useOpenInAppsStore';
import { useI18n } from '@/lib/i18n';
@@ -87,7 +87,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
initialize();
}, [initialize]);
const isDesktopLocal = isTauriShell() && isDesktopLocalOriginActive();
const isDesktopLocal = isDesktopShell() && isDesktopLocalOriginActive();
const selectedApp = React.useMemo(() => {
const known = availableApps.find((app) => app.id === selectedAppId)
+6 -19
View File
@@ -1660,15 +1660,12 @@ export const Header: React.FC<HeaderProps> = ({
}
let disposed = false;
let unlistenResize: (() => void) | null = null;
const syncFullscreenState = async () => {
try {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
const fullscreen = await currentWindow.isFullscreen();
const fullscreen = await invokeDesktop<boolean>('desktop_is_window_fullscreen');
if (!disposed) {
setIsDesktopWindowFullscreen(fullscreen);
setIsDesktopWindowFullscreen(fullscreen === true);
}
} catch {
if (!disposed) {
@@ -1677,26 +1674,16 @@ export const Header: React.FC<HeaderProps> = ({
}
};
const attach = async () => {
try {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
unlistenResize = await currentWindow.onResized(() => {
void syncFullscreenState();
});
} catch {
// Ignore listener setup failures; fallback state remains false.
}
const onResize = () => {
void syncFullscreenState();
};
void syncFullscreenState();
void attach();
window.addEventListener('openchamber:window-resized', onResize);
return () => {
disposed = true;
if (unlistenResize) {
unlistenResize();
}
window.removeEventListener('openchamber:window-resized', onResize);
};
}, [isDesktopApp, isMacPlatform]);
@@ -1,5 +1,5 @@
import React from 'react';
import { isDesktopShell, isTauriShell, startDesktopWindowDrag } from '@/lib/desktop';
import { isDesktopShell, requestFileAccess, startDesktopWindowDrag } from '@/lib/desktop';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Icon } from "@/components/icon/Icon";
@@ -116,7 +116,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
}, []);
const persistFirstChoice = React.useCallback(async (choice: 'local' | 'remote') => {
if (!isTauriShell()) return;
if (!isDesktopApp) return;
const config = await desktopHostsGet();
await desktopHostsSet({
@@ -124,14 +124,14 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
...(choice === 'local' ? { defaultHostId: 'local' } : {}),
initialHostChoiceCompleted: true,
});
}, []);
}, [isDesktopApp]);
const announceAvailable = React.useCallback(async () => {
if (isTauriShell()) {
if (isDesktopApp) {
await persistFirstChoice('local');
}
onCliAvailable?.();
}, [onCliAvailable, persistFirstChoice]);
}, [isDesktopApp, onCliAvailable, persistFirstChoice]);
// Background polling: while the local tab is visible, periodically check
// whether the OpenCode CLI is reachable. As soon as it is, transition
@@ -179,30 +179,23 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
const handleBrowse = React.useCallback(async () => {
if (typeof window === 'undefined') return;
if (!isDesktopApp || !isTauriShell()) return;
const tauri = (window as unknown as { __TAURI__?: { dialog?: { open?: (opts: Record<string, unknown>) => Promise<unknown> } } }).__TAURI__;
if (!tauri?.dialog?.open) return;
if (!isDesktopApp) return;
try {
const selected = await tauri.dialog.open({
title: t('onboarding.localSetup.dialog.selectOpencodeBinary'),
multiple: false,
directory: false,
});
if (typeof selected === 'string' && selected.trim().length > 0) {
setOpencodeBinary(selected.trim());
const selected = await requestFileAccess();
if (selected.success && selected.path && selected.path.trim().length > 0) {
setOpencodeBinary(selected.path.trim());
}
} catch {
// ignore
}
}, [isDesktopApp, t]);
}, [isDesktopApp]);
const handleApplyPath = React.useCallback(async () => {
setIsApplyingPath(true);
try {
await updateDesktopSettings({ opencodeBinary: opencodeBinary.trim() });
if (isTauriShell()) {
if (isDesktopApp) {
await persistFirstChoice('local');
await restartDesktopApp();
return;
@@ -211,7 +204,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
} finally {
setTimeout(() => setIsApplyingPath(false), 1000);
}
}, [opencodeBinary, persistFirstChoice]);
}, [isDesktopApp, opencodeBinary, persistFirstChoice]);
const handleCopy = React.useCallback(async () => {
const result = await copyTextToClipboard(INSTALL_COMMAND);
@@ -231,7 +224,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
? '/home/you/.bun/bin/opencode'
: '/Users/you/.bun/bin/opencode';
const showLocal = !isDesktopApp || !isTauriShell() || activeTab === 'local';
const showLocal = !isDesktopApp || activeTab === 'local';
return (
<div
@@ -248,7 +241,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
</p>
</header>
{isDesktopApp && isTauriShell() && (
{isDesktopApp && (
<div className="app-region-no-drag flex gap-1.5">
<button
type="button"
@@ -277,7 +270,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
</div>
)}
{isDesktopApp && isTauriShell() && activeTab === 'remote' ? (
{isDesktopApp && activeTab === 'remote' ? (
<div className="app-region-no-drag">
<RemoteConnectionForm
onBack={() => setActiveTab('local')}
@@ -394,7 +387,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
variant="secondary"
size="sm"
onClick={handleBrowse}
disabled={isApplyingPath || !isDesktopApp || !isTauriShell()}
disabled={isApplyingPath || !isDesktopApp}
>
{t('onboarding.localSetup.actions.browse')}
</Button>
@@ -1,5 +1,5 @@
import React from 'react';
import { isDesktopShell, isTauriShell } from '@/lib/desktop';
import { isDesktopShell, requestFileAccess, startDesktopWindowDrag } from '@/lib/desktop';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Icon } from "@/components/icon/Icon";
@@ -122,14 +122,8 @@ export function LocalSetupScreen({
return;
}
if (e.button !== 0) return;
if (isDesktopApp && isTauriShell()) {
try {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const window = getCurrentWindow();
await window.startDragging();
} catch (error) {
console.error('Failed to start window dragging:', error);
}
if (isDesktopApp) {
await startDesktopWindowDrag();
}
}, [isDesktopApp]);
@@ -148,37 +142,28 @@ export function LocalSetupScreen({
if (typeof window === 'undefined') {
return;
}
if (!isDesktopApp || !isTauriShell()) {
return;
}
const tauri = (window as unknown as { __TAURI__?: { dialog?: { open?: (opts: Record<string, unknown>) => Promise<unknown> } } }).__TAURI__;
if (!tauri?.dialog?.open) {
if (!isDesktopApp) {
return;
}
try {
const selected = await tauri.dialog.open({
title: t('onboarding.localSetup.dialog.selectOpencodeBinary'),
multiple: false,
directory: false,
});
if (typeof selected === 'string' && selected.trim().length > 0) {
setOpencodeBinary(selected.trim());
const selected = await requestFileAccess();
if (selected.success && selected.path && selected.path.trim().length > 0) {
setOpencodeBinary(selected.path.trim());
}
} catch {
// ignore
}
}, [isDesktopApp, t]);
}, [isDesktopApp]);
const handleApplyPath = React.useCallback(async () => {
setIsRetrying(true);
try {
await updateDesktopSettings({ opencodeBinary: opencodeBinary.trim() });
// In desktop boot flow, always restart the entire Tauri app so Rust
// can re-evaluate the boot outcome with the updated binary path.
if (isTauriShell()) {
// In desktop boot flow, restart the app so the native host can
// re-evaluate the boot outcome with the updated binary path.
if (isDesktopApp) {
await restartDesktopApp();
return;
}
@@ -187,7 +172,7 @@ export function LocalSetupScreen({
} finally {
setTimeout(() => setIsRetrying(false), 1000);
}
}, [opencodeBinary]);
}, [isDesktopApp, opencodeBinary]);
const handleCopy = React.useCallback(async () => {
const result = await copyTextToClipboard(INSTALL_COMMAND);
@@ -321,7 +306,7 @@ export function LocalSetupScreen({
type="button"
variant="secondary"
onClick={handleBrowse}
disabled={isRetrying || !isDesktopApp || !isTauriShell()}
disabled={isRetrying || !isDesktopApp}
>
{t('onboarding.localSetup.actions.browse')}
</Button>
@@ -1,5 +1,5 @@
import React from 'react';
import { isTauriShell, restartDesktopApp } from '@/lib/desktop';
import { isDesktopShell, restartDesktopApp } from '@/lib/desktop';
import { DesktopConnectionRecovery, type RecoveryVariant } from './DesktopConnectionRecovery';
import { RemoteConnectionForm } from './RemoteConnectionForm';
import { resolveRecoveryNextStep } from './desktopRecoveryRouting';
@@ -43,7 +43,7 @@ export function RecoveryScreen({
}: RecoveryScreenProps) {
// Persist the user's first choice (local or remote)
const persistFirstChoice = React.useCallback(async (choice: 'local' | 'remote') => {
if (!isTauriShell()) return;
if (!isDesktopShell()) return;
const config = await desktopHostsGet();
await desktopHostsSet({
@@ -56,9 +56,9 @@ export function RecoveryScreen({
}, []);
const handleRecoveryRetry = React.useCallback(async () => {
// In desktop boot flow, always restart the entire Tauri app so Rust
// can re-evaluate the boot outcome.
if (isTauriShell()) {
// In desktop boot flow, restart the app so the native host can
// re-evaluate the boot outcome.
if (isDesktopShell()) {
await restartDesktopApp();
return;
}
@@ -77,7 +77,7 @@ export function RecoveryScreen({
// switch-default-to-local → persist local choice and restart
await persistFirstChoice('local');
if (isTauriShell()) {
if (isDesktopShell()) {
await restartDesktopApp();
return;
}
@@ -105,7 +105,7 @@ export function RecoveryScreen({
isRecoveryMode={true}
onSwitchToLocal={onSwitchToLocalFromRemote || (() => {
persistFirstChoice('local').then(() => {
if (isTauriShell()) {
if (isDesktopShell()) {
restartDesktopApp();
} else {
onEnterLocalSetup?.();
@@ -8,7 +8,7 @@ import {
} from '@/lib/desktopHosts';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { isTauriShell } from '@/lib/desktop';
import { isDesktopShell, restartDesktopApp } from '@/lib/desktop';
import { useI18n } from '@/lib/i18n';
type ConnectionState = 'idle' | 'testing' | 'success' | 'error';
@@ -153,9 +153,8 @@ export function RemoteConnectionForm({
return;
}
if (isTauriShell()) {
const tauri = (window as unknown as { __TAURI__?: { core?: { invoke?: (cmd: string, args?: Record<string, unknown>) => Promise<unknown> } } }).__TAURI__;
await tauri?.core?.invoke?.('desktop_restart');
if (isDesktopShell()) {
await restartDesktopApp();
}
} catch (err) {
setError(err instanceof Error ? err.message : t('onboarding.remoteConnection.errors.failedToSaveConnection'));
@@ -4,7 +4,7 @@ import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import { isDesktopShell, isTauriShell } from '@/lib/desktop';
import { isDesktopShell, requestFileAccess } from '@/lib/desktop';
import { updateDesktopSettings } from '@/lib/persistence';
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
import { useUIStore } from '@/stores/useUIStore';
@@ -54,28 +54,19 @@ export const OpenCodeCliSettings: React.FC = () => {
return;
}
if (!isDesktopShell() || !isTauriShell()) {
return;
}
const tauri = (window as unknown as { __TAURI__?: { dialog?: { open?: (opts: Record<string, unknown>) => Promise<unknown> } } }).__TAURI__;
if (!tauri?.dialog?.open) {
if (!isDesktopShell()) {
return;
}
try {
const selected = await tauri.dialog.open({
title: t('settings.openchamber.opencodeCli.dialog.selectBinaryTitle'),
multiple: false,
directory: false,
});
if (typeof selected === 'string' && selected.trim().length > 0) {
setValue(selected.trim());
const selected = await requestFileAccess();
if (selected.success && selected.path && selected.path.trim().length > 0) {
setValue(selected.path.trim());
}
} catch {
// ignore
}
}, [t]);
}, []);
const handleSaveAndReload = React.useCallback(async () => {
setIsSaving(true);
@@ -130,7 +121,7 @@ export const OpenCodeCliSettings: React.FC = () => {
variant="outline"
size="xs"
onClick={handleBrowse}
disabled={isLoading || isSaving || !isDesktopShell() || !isTauriShell()}
disabled={isLoading || isSaving || !isDesktopShell()}
className="h-7 w-7 p-0"
aria-label={t('settings.openchamber.opencodeCli.actions.browseAria')}
title={t('settings.openchamber.opencodeCli.actions.browse')}
@@ -42,7 +42,6 @@ import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { EditorView } from '@codemirror/view';
import type { Extension } from '@codemirror/state';
import { convertFileSrc } from '@tauri-apps/api/core';
import { useThemeSystem } from '@/contexts/useThemeSystem';
import { useUIStore } from '@/stores/useUIStore';
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
@@ -2629,7 +2628,10 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const srcPromise = files.readFileBinary
? files.readFileBinary(selectedFile.path, selectedFileReadOptions).then((result) => result.dataUrl)
: Promise.resolve(convertFileSrc(selectedFile.path, 'asset'));
: Promise.resolve(getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', {
path: selectedFile.path,
allowOutsideWorkspace: selectedFileReadOptions.allowOutsideWorkspace ? 'true' : undefined,
}));
await srcPromise
.then((src) => {
@@ -7,7 +7,7 @@ import React, {
import { flushSync } from 'react-dom';
import type { Theme, ThemeMode } from '@/types/theme';
import type { DesktopSettings } from '@/lib/desktop';
import { isDesktopLocalOriginActive, isTauriShell, isVSCodeRuntime } from '@/lib/desktop';
import { isDesktopLocalOriginActive, isDesktopShell as detectDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
import { setDesktopWindowTheme } from '@/lib/desktopNative';
import { CSSVariableGenerator } from '@/lib/theme/cssGenerator';
import { updateDesktopSettings } from '@/lib/persistence';
@@ -220,7 +220,7 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
});
const isVSCode = useMemo(() => isVSCodeRuntime(), []);
const isLocalDesktopOrigin = useMemo(() => isDesktopLocalOriginActive(), []);
const isDesktopShell = useMemo(() => isTauriShell(), []);
const isDesktopShell = useMemo(() => detectDesktopShell(), []);
const availableThemes = useMemo(() => {
const merged: Theme[] = [];
+3 -7
View File
@@ -56,17 +56,13 @@ const copyCurrentSelectionFallback = async (): Promise<boolean> => {
const MENU_ACTION_EVENT = 'openchamber:menu-action';
const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates';
type TauriEventApi = {
type DesktopBridgeGlobal = {
listen?: (
event: string,
handler: (evt: { payload?: unknown }) => void
) => Promise<() => void>;
};
type TauriGlobal = {
event?: TauriEventApi;
};
type MenuAction =
| 'about'
| 'settings'
@@ -344,8 +340,8 @@ export const useMenuActions = (
React.useEffect(() => {
if (typeof window === 'undefined') return;
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const listen = tauri?.event?.listen;
const desktop = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__;
const listen = desktop?.listen;
if (typeof listen !== 'function') return;
let unlistenMenu: null | (() => void | Promise<void>) = null;
+1 -1
View File
@@ -27,7 +27,7 @@ function isVSCodeContext(): boolean {
*
* Works in:
* - Web: Full bidirectional sync
* - Desktop (Tauri): Full bidirectional sync
* - Desktop: Full bidirectional sync
* - VS Code: State-only (no URL updates, reads initial params)
*/
export function useRouter(): void {
+2 -2
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell } from '@/lib/desktop';
import { isDesktopLocalOriginActive, isDesktopShell } from '@/lib/desktop';
import { desktopHostsGet, getDesktopHostApiUrl, locationMatchesHost, redactSensitiveUrl } from '@/lib/desktopHosts';
import { setDesktopWindowTitle } from '@/lib/desktopNative';
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
@@ -110,7 +110,7 @@ export const useWindowTitle = () => {
document.title = title;
}
if (!isTauriShell()) {
if (!isDesktopShell()) {
return;
}
+2 -2
View File
@@ -221,7 +221,7 @@ export const debugUtils = {
})();
const runtimeApis = getRegisteredRuntimeAPIs();
const isTauriShell = typeof window !== 'undefined' && Boolean((window as any).__TAURI__);
const isDesktopRuntime = typeof window !== 'undefined' && Boolean((window as { __OPENCHAMBER_ELECTRON__?: unknown }).__OPENCHAMBER_ELECTRON__);
const safeJson = async (resp: Response) => {
try {
@@ -327,7 +327,7 @@ export const debugUtils = {
const report = {
runtime: {
platform: runtimeApis?.runtime?.platform ?? null,
isDesktop: isTauriShell,
isDesktop: isDesktopRuntime,
isVSCode: Boolean(runtimeApis?.runtime?.isVSCode),
hasRuntimeApis: Boolean(runtimeApis),
desktopServerOrigin: null,
+66 -92
View File
@@ -185,19 +185,14 @@ export type DesktopSettings = {
draftStarters?: DraftStarterRef[];
};
type TauriGlobal = {
core?: {
invoke?: (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
};
dialog?: {
open?: (options: Record<string, unknown>) => Promise<unknown>;
};
event?: {
listen?: (
event: string,
handler: (evt: { payload?: unknown }) => void,
) => Promise<() => void>;
};
type DesktopBridgeGlobal = {
invoke?: (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
openDialog?: (options: Record<string, unknown>) => Promise<unknown>;
openExternal?: (url: string) => Promise<unknown>;
listen?: (
event: string,
handler: (evt: { payload?: unknown }) => void,
) => Promise<() => void>;
};
type ElectronRuntimeGlobal = {
@@ -209,27 +204,23 @@ const getElectronRuntime = (): ElectronRuntimeGlobal | null => {
return (window as unknown as { __OPENCHAMBER_ELECTRON__?: ElectronRuntimeGlobal }).__OPENCHAMBER_ELECTRON__ ?? null;
};
export const isTauriShell = (): boolean => {
if (typeof window === 'undefined') return false;
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
return typeof tauri?.core?.invoke === 'function';
const getDesktopBridge = (): DesktopBridgeGlobal | null => {
if (typeof window === 'undefined') return null;
return (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__ ?? null;
};
export const isElectronShell = (): boolean => getElectronRuntime()?.runtime === 'electron';
export const hasDesktopInvoke = (): boolean => {
if (typeof window === 'undefined') return false;
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
return typeof tauri?.core?.invoke === 'function';
return typeof getDesktopBridge()?.invoke === 'function';
};
export const canUseElectronDesktopIPC = (): boolean => isElectronShell() && hasDesktopInvoke();
export const invokeDesktop = async <T = unknown>(command: string, args?: Record<string, unknown>): Promise<T | null> => {
if (typeof window === 'undefined') return null;
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
if (typeof tauri?.core?.invoke !== 'function') return null;
return tauri.core.invoke(command, args ?? {}) as Promise<T>;
const bridge = getDesktopBridge();
if (typeof bridge?.invoke !== 'function') return null;
return bridge.invoke(command, args ?? {}) as Promise<T>;
};
type LaunchAtLoginStatus = {
@@ -367,18 +358,16 @@ export const isDesktopLocalOriginActive = (): boolean => {
export const isDesktopShell = (): boolean => {
if (typeof window === 'undefined') return false;
return isTauriShell() || isElectronShell();
return isElectronShell();
};
export const startDesktopWindowDrag = async (): Promise<boolean> => {
if (!isDesktopShell() || !isTauriShell()) {
if (!isDesktopShell()) {
return false;
}
try {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const appWindow = getCurrentWindow();
await appWindow.startDragging();
await invokeDesktop('desktop_start_window_drag');
return true;
} catch {
return false;
@@ -418,10 +407,9 @@ export const requestDirectoryAccess = async (
directoryPath: string
): Promise<{ success: boolean; path?: string; projectId?: string; error?: string }> => {
// Desktop shell on local instance: use native folder picker.
if (isTauriShell() && isDesktopLocalOriginActive()) {
if (hasDesktopInvoke() && isDesktopLocalOriginActive()) {
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const selected = await tauri?.dialog?.open?.({
const selected = await getDesktopBridge()?.openDialog?.({
directory: true,
multiple: false,
title: 'Select Working Directory',
@@ -431,7 +419,7 @@ export const requestDirectoryAccess = async (
}
return { success: true, path: selected };
} catch (error) {
console.warn('Failed to request directory access (tauri)', error);
console.warn('Failed to request directory access', error);
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
@@ -442,10 +430,9 @@ export const requestDirectoryAccess = async (
export const requestFileAccess = async (
options?: { filters?: Array<{ name: string; extensions: string[] }>; defaultPath?: string }
): Promise<{ success: boolean; path?: string; error?: string }> => {
if (isTauriShell() && isDesktopLocalOriginActive()) {
if (hasDesktopInvoke() && isDesktopLocalOriginActive()) {
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const selected = await tauri?.dialog?.open?.({
const selected = await getDesktopBridge()?.openDialog?.({
directory: false,
multiple: false,
title: 'Select File',
@@ -457,7 +444,7 @@ export const requestFileAccess = async (
}
return { success: true, path: selected };
} catch (error) {
console.warn('Failed to request file access (tauri)', error);
console.warn('Failed to request file access', error);
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
@@ -482,10 +469,9 @@ export const stopAccessingDirectory = async (
export const sendAssistantCompletionNotification = async (
payload?: AssistantNotificationPayload
): Promise<boolean> => {
if (isTauriShell()) {
if (hasDesktopInvoke()) {
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
await tauri?.core?.invoke?.('desktop_notify', {
await invokeDesktop('desktop_notify', {
payload: {
title: payload?.title,
body: payload?.body,
@@ -494,7 +480,7 @@ export const sendAssistantCompletionNotification = async (
});
return true;
} catch (error) {
console.warn('Failed to send assistant completion notification (tauri)', error);
console.warn('Failed to send assistant completion notification', error);
return false;
}
}
@@ -503,16 +489,15 @@ export const sendAssistantCompletionNotification = async (
};
export const checkForDesktopUpdates = async (): Promise<UpdateInfo | null> => {
if (!isTauriShell()) {
if (!hasDesktopInvoke()) {
return null;
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const info = await tauri?.core?.invoke?.('desktop_check_for_updates');
const info = await invokeDesktop<UpdateInfo>('desktop_check_for_updates');
return info as UpdateInfo;
} catch (error) {
console.warn('Failed to check for updates (tauri)', error);
console.warn('Failed to check for updates', error);
return null;
}
};
@@ -520,18 +505,18 @@ export const checkForDesktopUpdates = async (): Promise<UpdateInfo | null> => {
export const downloadDesktopUpdate = async (
onProgress?: (progress: UpdateProgress) => void
): Promise<boolean> => {
if (!isTauriShell()) {
if (!hasDesktopInvoke()) {
return false;
}
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const bridge = getDesktopBridge();
let unlisten: null | (() => void | Promise<void>) = null;
let downloaded = 0;
let total: number | undefined;
try {
if (typeof onProgress === 'function' && tauri?.event?.listen) {
unlisten = await tauri.event.listen('openchamber:update-progress', (evt) => {
if (typeof onProgress === 'function' && bridge?.listen) {
unlisten = await bridge.listen('openchamber:update-progress', (evt) => {
const payload = evt?.payload;
if (!payload || typeof payload !== 'object') return;
const data = payload as { event?: unknown; data?: unknown };
@@ -560,10 +545,10 @@ export const downloadDesktopUpdate = async (
});
}
await tauri?.core?.invoke?.('desktop_download_and_install_update');
await invokeDesktop('desktop_download_and_install_update');
return true;
} catch (error) {
console.warn('Failed to download update (tauri)', error);
console.warn('Failed to download update', error);
return false;
} finally {
if (unlisten) {
@@ -580,7 +565,7 @@ export const downloadDesktopUpdate = async (
};
export const restartToApplyUpdate = async (): Promise<boolean> => {
if (!isTauriShell()) {
if (!hasDesktopInvoke()) {
return false;
}
@@ -588,37 +573,35 @@ export const restartToApplyUpdate = async (): Promise<boolean> => {
};
export const restartDesktopApp = async (): Promise<boolean> => {
if (!isTauriShell()) {
if (!hasDesktopInvoke()) {
return false;
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
await tauri?.core?.invoke?.('desktop_restart');
await invokeDesktop('desktop_restart');
return true;
} catch (error) {
console.warn('Failed to restart desktop app (tauri)', error);
console.warn('Failed to restart desktop app', error);
return false;
}
};
export const getDesktopLanAddress = async (): Promise<string | null> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return null;
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const result = await tauri?.core?.invoke?.('desktop_get_lan_address');
const result = await invokeDesktop<string>('desktop_get_lan_address');
return typeof result === 'string' && result.trim().length > 0 ? result.trim() : null;
} catch (error) {
console.warn('Failed to get desktop LAN address (tauri)', error);
console.warn('Failed to get desktop LAN address', error);
return null;
}
};
export const openDesktopPath = async (path: string, app?: string | null): Promise<boolean> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return false;
}
@@ -628,20 +611,19 @@ export const openDesktopPath = async (path: string, app?: string | null): Promis
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
await tauri?.core?.invoke?.('desktop_open_path', {
await invokeDesktop('desktop_open_path', {
path: trimmed,
app: typeof app === 'string' && app.trim().length > 0 ? app.trim() : undefined,
});
return true;
} catch (error) {
console.warn('Failed to open path (tauri)', error);
console.warn('Failed to open path', error);
return false;
}
};
export const revealDesktopPath = async (path: string): Promise<boolean> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return false;
}
@@ -651,8 +633,7 @@ export const revealDesktopPath = async (path: string): Promise<boolean> => {
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
await tauri?.core?.invoke?.('desktop_reveal_path', {
await invokeDesktop('desktop_reveal_path', {
path: trimmed,
});
return true;
@@ -665,7 +646,7 @@ export const saveDesktopMarkdownFile = async (
defaultFileName: string,
content: string,
): Promise<string | null> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return null;
}
@@ -675,14 +656,13 @@ export const saveDesktopMarkdownFile = async (
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const result = await tauri?.core?.invoke?.('desktop_save_markdown_file', {
const result = await invokeDesktop<string>('desktop_save_markdown_file', {
defaultFileName: trimmedFileName,
content,
});
return typeof result === 'string' && result.trim().length > 0 ? result : null;
} catch (error) {
console.warn('Failed to save markdown file (tauri)', error);
console.warn('Failed to save markdown file', error);
return null;
}
};
@@ -692,7 +672,7 @@ export const openDesktopProjectInApp = async (
appId: string,
appName: string,
): Promise<boolean> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return false;
}
@@ -705,8 +685,7 @@ export const openDesktopProjectInApp = async (
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
await tauri?.core?.invoke?.('desktop_open_in_app', {
await invokeDesktop('desktop_open_in_app', {
projectPath: trimmedProjectPath,
appId: trimmedAppId,
appName: trimmedAppName,
@@ -723,7 +702,7 @@ export const openDesktopFileInApp = async (
appId: string,
appName: string,
): Promise<boolean> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return false;
}
@@ -736,8 +715,7 @@ export const openDesktopFileInApp = async (
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
await tauri?.core?.invoke?.('desktop_open_file_in_app', {
await invokeDesktop('desktop_open_file_in_app', {
filePath: trimmedFilePath,
appId: trimmedAppId,
appName: trimmedAppName,
@@ -750,7 +728,7 @@ export const openDesktopFileInApp = async (
};
export const filterInstalledDesktopApps = async (apps: string[]): Promise<string[]> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return [];
}
@@ -760,19 +738,18 @@ export const filterInstalledDesktopApps = async (apps: string[]): Promise<string
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const result = await tauri?.core?.invoke?.('desktop_filter_installed_apps', {
const result = await invokeDesktop<string[]>('desktop_filter_installed_apps', {
apps: candidate,
});
return Array.isArray(result) ? result.filter((value) => typeof value === 'string') : [];
} catch (error) {
console.warn('Failed to check installed apps (tauri)', error);
console.warn('Failed to check installed apps', error);
return [];
}
};
export const fetchDesktopAppIcons = async (apps: string[]): Promise<Record<string, string>> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return {};
}
@@ -782,8 +759,7 @@ export const fetchDesktopAppIcons = async (apps: string[]): Promise<Record<strin
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const result = await tauri?.core?.invoke?.('desktop_fetch_app_icons', {
const result = await invokeDesktop<unknown[]>('desktop_fetch_app_icons', {
apps: candidate,
});
if (!Array.isArray(result)) {
@@ -798,7 +774,7 @@ export const fetchDesktopAppIcons = async (apps: string[]): Promise<Record<strin
}
return map;
} catch (error) {
console.warn('Failed to fetch installed app icons (tauri)', error);
console.warn('Failed to fetch installed app icons', error);
return {};
}
};
@@ -819,7 +795,7 @@ export const fetchDesktopInstalledApps = async (
apps: string[],
force?: boolean
): Promise<FetchDesktopInstalledAppsResult> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return { apps: [], success: false, hasCache: false, isCacheStale: false };
}
@@ -829,8 +805,7 @@ export const fetchDesktopInstalledApps = async (
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const result = await tauri?.core?.invoke?.('desktop_get_installed_apps', {
const result = await invokeDesktop<unknown>('desktop_get_installed_apps', {
apps: candidate,
force: force === true ? true : undefined,
});
@@ -858,19 +833,18 @@ export const fetchDesktopInstalledApps = async (
isCacheStale: payload.isCacheStale === true,
};
} catch (error) {
console.warn('Failed to fetch installed apps (tauri)', error);
console.warn('Failed to fetch installed apps', error);
return { apps: [], success: false, hasCache: false, isCacheStale: false };
}
};
export const clearDesktopCache = async (): Promise<boolean> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return false;
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
await tauri?.core?.invoke?.('desktop_clear_cache');
await invokeDesktop('desktop_clear_cache');
return true;
} catch (error) {
console.warn('Failed to clear cache', error);
+4 -4
View File
@@ -194,7 +194,7 @@ describe('shouldRestartDesktopBootFlow', () => {
test('restarts the desktop app when boot UI is running in the startup window', () => {
expect(
shouldRestartDesktopBootFlow({
isTauriShell: true,
isDesktopShell: true,
isDesktopLocalOriginActive: false,
}),
).toBe(true);
@@ -203,16 +203,16 @@ describe('shouldRestartDesktopBootFlow', () => {
test('does not restart when the local desktop origin is already active', () => {
expect(
shouldRestartDesktopBootFlow({
isTauriShell: true,
isDesktopShell: true,
isDesktopLocalOriginActive: true,
}),
).toBe(false);
});
test('does not restart outside the tauri shell', () => {
test('does not restart outside the desktop shell', () => {
expect(
shouldRestartDesktopBootFlow({
isTauriShell: false,
isDesktopShell: false,
isDesktopLocalOriginActive: false,
}),
).toBe(false);
+6 -6
View File
@@ -222,14 +222,14 @@ export type InitialLoadingState = {
};
export type DesktopBootFlowRestartInput = {
isTauriShell: boolean;
isDesktopShell: boolean;
isDesktopLocalOriginActive: boolean;
};
/**
* Whether the initial loading screen can be dismissed.
*
* Desktop shells must wait until a valid boot outcome is injected by Rust.
* Desktop shells must wait until a valid boot outcome is injected by the native host.
* For non-main views (chooser, recovery), the splash can dismiss as soon as
* the outcome is known `isInitialized` is not required because OpenCode
* may not be available in those flows.
@@ -254,16 +254,16 @@ export function canDismissInitialLoading(state: InitialLoadingState): boolean {
}
/**
* Boot/recovery UI can render in the Tauri startup window before the local
* Boot/recovery UI can render in the desktop startup window before the local
* desktop HTTP origin is active. In that state, same-origin reloads and
* `/api/*` requests cannot recover the app, so callers must restart Tauri.
* `/api/*` requests cannot recover the app, so callers must restart desktop.
*/
export function shouldRestartDesktopBootFlow(input: DesktopBootFlowRestartInput): boolean {
return input.isTauriShell && !input.isDesktopLocalOriginActive;
return input.isDesktopShell && !input.isDesktopLocalOriginActive;
}
/**
* Read the boot outcome injected by the Rust backend.
* Read the boot outcome injected by the native desktop host.
* Returns `null` when not in desktop, when the outcome has not been set yet,
* or when the injected payload is malformed.
*/
+5 -12
View File
@@ -1,12 +1,6 @@
import { isTauriShell } from '@/lib/desktop';
import { hasDesktopInvoke, invokeDesktop } from '@/lib/desktop';
type TauriInvoke = (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
type TauriGlobal = {
core?: {
invoke?: TauriInvoke;
};
};
type DesktopInvoke = (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
export type DesktopHost = {
id: string;
@@ -176,10 +170,9 @@ export const getDesktopHostApiUrl = (host: DesktopHost): string => {
return normalizeHostUrl(host.apiUrl || host.url) || host.apiUrl || host.url;
};
const getInvoke = (): TauriInvoke | null => {
if (!isTauriShell()) return null;
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
return typeof tauri?.core?.invoke === 'function' ? tauri.core.invoke : null;
const getInvoke = (): DesktopInvoke | null => {
if (!hasDesktopInvoke()) return null;
return (command, args) => invokeDesktop(command, args) as Promise<unknown>;
};
export const desktopHostsGet = async (): Promise<DesktopHostsConfig> => {
+6 -60
View File
@@ -1,30 +1,15 @@
import { isDesktopShell } from '@/lib/desktop';
import { hasDesktopInvoke, invokeDesktop, isDesktopShell } from '@/lib/desktop';
type InvokeArgs = Record<string, unknown>;
const isElectronDesktop = (): boolean => {
return typeof window !== 'undefined' && Boolean((window as { __OPENCHAMBER_ELECTRON__?: unknown }).__OPENCHAMBER_ELECTRON__);
};
const getInvoke = () => {
if (typeof window === 'undefined') {
return null;
}
const tauri = (window as unknown as {
__TAURI__?: { core?: { invoke?: (cmd: string, args?: InvokeArgs) => Promise<unknown> } };
}).__TAURI__;
return typeof tauri?.core?.invoke === 'function' ? tauri.core.invoke : null;
};
export const invokeDesktopCommand = async <TValue = unknown>(
command: string,
args?: InvokeArgs,
): Promise<TValue> => {
const invoke = getInvoke();
if (!invoke) {
if (!hasDesktopInvoke()) {
throw new Error('Desktop runtime is not available');
}
return invoke(command, args) as Promise<TValue>;
return invokeDesktop<TValue>(command, args) as Promise<TValue>;
};
export const startDesktopWindowDrag = async (): Promise<void> => {
@@ -33,13 +18,7 @@ export const startDesktopWindowDrag = async (): Promise<void> => {
}
try {
if (isElectronDesktop()) {
await invokeDesktopCommand('desktop_start_window_drag');
return;
}
const { getCurrentWindow } = await import('@tauri-apps/api/window');
await getCurrentWindow().startDragging();
await invokeDesktopCommand('desktop_start_window_drag');
} catch {
// ignore
}
@@ -51,11 +30,6 @@ export const isDesktopWindowFullscreen = async (): Promise<boolean> => {
}
try {
if (!isElectronDesktop()) {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
return await getCurrentWindow().isFullscreen();
}
return Boolean(await invokeDesktopCommand('desktop_is_window_fullscreen'));
} catch {
return false;
@@ -77,12 +51,6 @@ export const setDesktopWindowTitle = async (title: string): Promise<void> => {
}
try {
if (!isElectronDesktop()) {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
await getCurrentWindow().setTitle(title);
return;
}
await invokeDesktopCommand('desktop_set_window_title', { title });
} catch {
// ignore
@@ -98,12 +66,6 @@ export const setDesktopWindowTheme = async (
}
try {
if (!isElectronDesktop()) {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('desktop_set_window_theme', { themeMode, themeVariant });
return;
}
await invokeDesktopCommand('desktop_set_window_theme', { themeMode, themeVariant });
} catch {
// ignore
@@ -116,11 +78,6 @@ export const getDesktopAppVersion = async (): Promise<string | null> => {
}
try {
if (!isElectronDesktop()) {
const { getVersion } = await import('@tauri-apps/api/app');
return await getVersion();
}
const version = await invokeDesktopCommand('desktop_get_app_version');
return typeof version === 'string' && version.trim().length > 0 ? version : null;
} catch {
@@ -146,17 +103,6 @@ export const listenDesktopNativeDragDrop = async (
return null;
}
// Electron uses the renderer's native DOM drag/drop events instead of a
// separate webview drag listener.
if (isElectronDesktop()) {
return null;
}
try {
const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow');
const webviewWindow = getCurrentWebviewWindow();
return await webviewWindow.onDragDropEvent(handler as never);
} catch {
return null;
}
void handler;
return null;
};
+13 -19
View File
@@ -1,17 +1,12 @@
import { isTauriShell } from '@/lib/desktop';
import { hasDesktopInvoke, invokeDesktop } from '@/lib/desktop';
type TauriInvoke = (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
type DesktopInvoke = (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
type TauriGlobal = {
core?: {
invoke?: TauriInvoke;
};
event?: {
listen?: (
event: string,
handler: (evt: { payload?: unknown }) => void,
) => Promise<() => void>;
};
type DesktopBridgeGlobal = {
listen?: (
event: string,
handler: (evt: { payload?: unknown }) => void,
) => Promise<() => void>;
};
export type DesktopSshRemoteMode = 'managed' | 'external';
@@ -126,10 +121,9 @@ const asStringArray = (value: unknown): string[] => {
return value.filter((item): item is string => typeof item === 'string');
};
const getInvoke = (): TauriInvoke | null => {
if (!isTauriShell()) return null;
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
return typeof tauri?.core?.invoke === 'function' ? tauri.core.invoke : null;
const getInvoke = (): DesktopInvoke | null => {
if (!hasDesktopInvoke()) return null;
return (command, args) => invokeDesktop(command, args) as Promise<unknown>;
};
const parseStoredSecret = (value: unknown): DesktopSshStoredSecret | undefined => {
@@ -432,12 +426,12 @@ export const desktopSshLogsClear = async (id: string): Promise<void> => {
export const listenDesktopSshStatus = async (
listener: (status: DesktopSshInstanceStatus) => void,
): Promise<() => Promise<void>> => {
if (!isTauriShell()) {
if (!hasDesktopInvoke()) {
return async () => {};
}
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const listen = tauri?.event?.listen;
const desktop = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__;
const listen = desktop?.listen;
if (typeof listen !== 'function') {
return async () => {};
}
+2 -2
View File
@@ -45,7 +45,7 @@ const getNavigatorDeviceHints = (maxTouchPoints: number) => {
};
const setRootDeviceAttributes = (
isTauriShellRuntime: boolean,
isDesktopShellRuntime: boolean,
deviceType: DeviceType,
hasTouchInput: boolean,
) => {
@@ -66,7 +66,7 @@ const setRootDeviceAttributes = (
: 'device-desktop'
);
if (isTauriShellRuntime) {
if (isDesktopShellRuntime) {
root.classList.add('desktop-runtime');
root.style.setProperty('--is-mobile', '0');
root.style.setProperty('--device-type', 'desktop');
+1 -2
View File
@@ -5,10 +5,9 @@ import type React from 'react';
* Uses both `isComposing` and the `keyCode === 229` fallback.
*
* Note: `keyCode` is deprecated, but `229` remains a practical fallback for
* some WebKit-based environments (including Tauri WebView) where composition
* some WebKit-based environments where composition
* events can be ordered unexpectedly.
*/
export const isIMECompositionEvent = (e: React.KeyboardEvent): boolean => {
return e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
};
@@ -1,5 +1,6 @@
import { snapdom } from '@zumer/snapdom';
import { getFontEmbedCSS, toJpeg } from 'html-to-image';
import { invokeDesktop } from '@/lib/desktop';
import { runtimeFetch } from '@/lib/runtime-fetch';
export type PreviewElementMetadata = {
@@ -97,18 +98,16 @@ export const renderPreviewScreenshot = async (
iframe: HTMLIFrameElement,
target: PreviewElementMetadata,
): Promise<File | null> => {
const tauri = typeof window !== 'undefined'
? (window as unknown as { __TAURI__?: { core?: { invoke?: <T>(cmd: string, args?: Record<string, unknown>) => Promise<T> } } }).__TAURI__
: undefined;
if (typeof tauri?.core?.invoke === 'function') {
if (typeof window !== 'undefined') {
try {
const rect = iframe.getBoundingClientRect();
const capture = await tauri.core.invoke<{ mime: string; base64: string; width: number; height: number }>('desktop_capture_page_rect', {
const capture = await invokeDesktop<{ mime: string; base64: string; width: number; height: number }>('desktop_capture_page_rect', {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
});
if (!capture) throw new Error('Desktop screenshot capture is not available');
const image = new Image();
await new Promise<void>((resolve, reject) => {
image.onload = () => resolve();
+1 -1
View File
@@ -2,7 +2,7 @@
* Router module for URL-based navigation in OpenChamber.
*
* Provides bidirectional sync between URL query parameters and application state.
* Works across web, desktop (Tauri), and VS Code (state-only mode).
* Works across web, desktop, and VS Code (state-only mode).
*
* URL Schema:
* - `?session=<id>` - Navigate to specific session
+4 -4
View File
@@ -1,5 +1,5 @@
import { isMacOS } from '@/lib/utils';
import { isTauriShell } from '@/lib/desktop';
import { isDesktopShell } from '@/lib/desktop';
export type ShortcutModifier = 'mod' | 'shift' | 'alt' | 'option' | 'ctrl';
export type ShortcutKey = string;
@@ -32,7 +32,7 @@ const MODIFIER_KEY_MAP: Record<string, ShortcutModifier> = {
};
const DISPLAY_LABEL_MAP: Record<ShortcutModifier, string> = {
'mod': isMacOS() && isTauriShell() ? '⌘' : 'Ctrl',
'mod': isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl',
'shift': '⇧',
'alt': '⌥',
'option': '⌥',
@@ -555,7 +555,7 @@ export function eventMatchesShortcut(
const expectedShift = parsed.modifiers.has('shift');
const expectedAlt = parsed.modifiers.has('alt');
const expectedCtrl = parsed.modifiers.has('ctrl');
const isDesktopMac = isMacOS() && isTauriShell();
const isDesktopMac = isMacOS() && isDesktopShell();
const isMac = isMacOS();
const modMatches = isDesktopMac
@@ -615,5 +615,5 @@ export function getShortcutLabel(id: string): string {
}
export function getModifierLabel(): string {
return isMacOS() && isTauriShell() ? '⌘' : 'Ctrl';
return isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl';
}
+6 -14
View File
@@ -1,15 +1,7 @@
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
/**
* Utility for opening external URLs with Tauri shell support.
* In desktop runtime, uses tauri.shell.open() for proper system browser handling.
* Falls back to window.open() for web runtime.
*/
type TauriShell = {
shell?: {
open?: (url: string) => Promise<unknown>;
};
type DesktopBridgeGlobal = {
openExternal?: (url: string) => Promise<unknown>;
};
const parseUrlSafely = (value: string): URL | null => {
@@ -90,7 +82,7 @@ export const extractLoopbackUrls = (text: string): string[] => {
/**
* Opens an external URL in the system browser.
* In Tauri desktop runtime, uses tauri.shell.open() for proper handling.
* In desktop runtime, uses the native shell for proper handling.
* Falls back to window.open() for web runtime.
*
* @param url - The URL to open
@@ -127,10 +119,10 @@ export const openExternalUrl = async (url: string): Promise<boolean> => {
}
}
const tauri = (window as unknown as { __TAURI__?: TauriShell }).__TAURI__;
if (tauri?.shell?.open) {
const desktop = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__;
if (desktop?.openExternal) {
try {
await tauri.shell.open(normalizedTarget);
await desktop.openExternal(normalizedTarget);
return true;
} catch {
// Fall through to window.open
+5 -5
View File
@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import { isTauriShell } from "@/lib/desktop";
import { isDesktopShell } from "@/lib/desktop";
import { matchesFuzzyQuery } from "@/lib/search/fuzzySearch";
import type { I18nKey } from "@/lib/i18n";
@@ -31,19 +31,19 @@ export const getRevealLabelKey = (): I18nKey => {
/**
* Checks if the platform-appropriate modifier key is pressed.
* On macOS desktop app: Cmd (metaKey), on other platforms or web: Ctrl (ctrlKey).
* Browser intercepts Cmd shortcuts, so we only use Cmd in Tauri desktop app.
* Browser intercepts Cmd shortcuts, so we only use Cmd in the desktop app.
*/
export const hasModifier = (e: KeyboardEvent | React.KeyboardEvent): boolean => {
return isMacOS() && isTauriShell() ? e.metaKey : e.ctrlKey;
return isMacOS() && isDesktopShell() ? e.metaKey : e.ctrlKey;
};
/**
* Returns the platform-appropriate modifier key label.
* On macOS desktop app: "⌘", on other platforms or web: "Ctrl"
* Browser intercepts Cmd shortcuts, so we only show Cmd in Tauri desktop app.
* Browser intercepts Cmd shortcuts, so we only show Cmd in the desktop app.
*/
export const getModifierLabel = (): string => {
return isMacOS() && isTauriShell() ? '⌘' : 'Ctrl';
return isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl';
};
export const truncatePathMiddle = (
+3 -3
View File
@@ -1,6 +1,6 @@
import { create } from 'zustand';
import { fetchDesktopInstalledApps, isDesktopLocalOriginActive, isTauriShell, type DesktopSettings, type InstalledDesktopAppInfo } from '@/lib/desktop';
import { fetchDesktopInstalledApps, isDesktopLocalOriginActive, isDesktopShell, type DesktopSettings, type InstalledDesktopAppInfo } from '@/lib/desktop';
import { OPEN_IN_APPS, DEFAULT_OPEN_IN_APP_ID, OPEN_IN_ALWAYS_AVAILABLE_APP_IDS, getOpenInAppById, getPlatformOpenInApp, type OpenInApp } from '@/lib/openInApps';
import { updateDesktopSettings } from '@/lib/persistence';
@@ -86,7 +86,7 @@ export const useOpenInAppsStore = create<OpenInAppsState>()((set, get) => ({
};
const loadInstalledApps = async (force?: boolean) => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
if (!isDesktopShell() || !isDesktopLocalOriginActive()) {
return;
}
@@ -213,7 +213,7 @@ export const useOpenInAppsStore = create<OpenInAppsState>()((set, get) => ({
get().initialize();
}
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
if (!isDesktopShell() || !isDesktopLocalOriginActive()) {
return;
}
+2 -3
View File
@@ -8,7 +8,6 @@ import {
restartToApplyUpdate,
isDesktopLocalOriginActive,
isElectronShell,
isTauriShell,
isVSCodeRuntime,
isWebRuntime,
} from '@/lib/desktop';
@@ -83,7 +82,7 @@ function mapRuntimeParams(runtime: ClientRuntime): URLSearchParams {
params.set('arch', detectArch());
params.set('platform', detectPlatform());
if (runtime === 'desktop') {
params.set('appType', isElectronShell() ? 'desktop-electron' : 'desktop-tauri');
params.set('appType', 'desktop-electron');
params.set('instanceMode', isDesktopLocalOriginActive() ? 'local' : 'remote');
return params;
}
@@ -136,7 +135,7 @@ async function checkForWebUpdates(runtime: ClientRuntime, currentVersion?: strin
}
function detectRuntimeType(): 'desktop' | 'web' | 'vscode' | null {
if (isTauriShell()) {
if (isElectronShell()) {
return 'desktop';
}
if (isVSCodeRuntime()) return 'vscode';
+2 -2
View File
@@ -51,8 +51,8 @@ const getOpenChamberConfigDir = (): string => {
return path.join(os.homedir(), '.config', 'openchamber');
};
const sanitizeInstallScope = (scope: string): 'desktop-tauri' | 'vscode' | 'web' => {
if (scope === 'desktop-tauri' || scope === 'vscode' || scope === 'web') return scope;
const sanitizeInstallScope = (scope: string): 'vscode' | 'web' => {
if (scope === 'vscode' || scope === 'web') return scope;
return 'web';
};
+1 -1
View File
@@ -683,7 +683,7 @@ export async function activate(context: vscode.ExtensionContext) {
sessionEditorProvider?.updateConnectionStatus(status, error);
// Start/stop global event watcher based on connection status
// Mirrors web server and desktop Tauri behavior
// Mirrors web server and desktop behavior
if (status === 'connected' && chatViewProvider && openCodeManager) {
setChatViewProvider(chatViewProvider);
void startGlobalEventWatcher(openCodeManager, chatViewProvider);
@@ -1,7 +1,7 @@
import { createOpencodeClient } from '@opencode-ai/sdk/v2';
import type { OpenCodeManager } from './opencode';
// Session activity tracking (mirrors web server and desktop Tauri behavior)
// Session activity tracking (mirrors web server and desktop behavior)
type ActivityPhase = 'idle' | 'busy' | 'cooldown';
interface SessionActivity {
@@ -46,8 +46,7 @@ export const createNotificationEmitterRuntime = (dependencies) => {
}
try {
// stdout IPC: Tauri shell spawns this process as a sidecar and parses
// its stdout for the one-line `${prefix}{json}` protocol.
// stdout fallback for runtimes that parse the one-line `${prefix}{json}` protocol.
process.stdout.write(`${desktopNotifyPrefix}${JSON.stringify(payload)}\n`);
} catch {
// ignore
@@ -64,9 +63,9 @@ export const createNotificationEmitterRuntime = (dependencies) => {
type: 'openchamber:notification',
properties: {
...payload,
// Tell the UI whether the sidecar stdout notification channel is active.
// Tell the UI whether the stdout notification channel is active.
// When true, the desktop UI should skip this SSE notification to avoid duplicates.
// When false (e.g. tauri dev), the UI must handle this SSE notification itself.
// When false, the UI must handle this SSE notification itself.
desktopStdoutActive: desktopNotifyEnabled,
},
};
+2 -2
View File
@@ -29,7 +29,7 @@ function getOpenChamberConfigDir() {
}
function sanitizeInstallScope(scope) {
if (scope === 'desktop-electron' || scope === 'desktop-tauri' || scope === 'vscode' || scope === 'web') return scope;
if (scope === 'desktop-electron' || scope === 'vscode' || scope === 'web') return scope;
return 'web';
}
@@ -65,7 +65,7 @@ function mapArch(value) {
}
function normalizeAppType(value) {
if (value === 'web' || value === 'desktop-electron' || value === 'desktop-tauri' || value === 'vscode') return value;
if (value === 'web' || value === 'desktop-electron' || value === 'vscode') return value;
return 'web';
}
@@ -115,27 +115,6 @@ describe('checkForUpdates', () => {
expect(result.available).toBe(false);
});
it('does not cross-check desktop update claims against npm', async () => {
fetchMock
.when('api.openchamber.dev', {
ok: true,
json: async () => ({
latestVersion: '1.10.0',
updateAvailable: true,
releaseNotes: '## [1.10.0] - 2026-05-01\n\n- Great new feature',
}),
});
const result = await checkForUpdates({
appType: 'desktop-tauri',
currentVersion: '1.9.10',
});
expect(result.available).toBe(true);
expect(result.version).toBe('1.10.0');
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('accepts electron desktop update claims without npm cross-checking', async () => {
fetchMock
.when('api.openchamber.dev', {
+7 -9
View File
@@ -184,13 +184,13 @@ const notifyWithDesktop = async (payload?: NotificationPayload): Promise<boolean
return false;
}
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
if (!tauri?.core?.invoke) {
const desktop = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__;
if (!desktop?.invoke) {
return false;
}
try {
await tauri.core.invoke('desktop_notify', {
await desktop.invoke('desktop_notify', {
payload: {
title: payload?.title,
body: payload?.body,
@@ -214,16 +214,14 @@ export const createWebNotificationsAPI = (): NotificationsAPI => ({
},
canNotify: () => {
if (typeof window !== 'undefined') {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
if (tauri?.core?.invoke) {
const desktop = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__;
if (desktop?.invoke) {
return true;
}
}
return typeof Notification !== 'undefined' ? Notification.permission === 'granted' : false;
},
});
type TauriGlobal = {
core?: {
invoke?: (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
};
type DesktopBridgeGlobal = {
invoke?: (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
};