Compare commits

...

14 Commits

Author SHA1 Message Date
simosmik
388134c7a5 chore(release): v1.28.0 2026-04-03 15:41:05 +00:00
simosmik
ef51de259e chore: changing package name to @cloudcli-ai/cloudcli 2026-04-03 15:37:49 +00:00
simosmik
1628868470 feat: moving new session button higher 2026-03-31 20:53:20 +00:00
simosmik
8f1042cf25 feat: adding session resume in the api 2026-03-29 20:58:56 +00:00
viper151
051a6b1e74 chore(release): v1.27.1 2026-03-29 01:15:38 +00:00
simosmik
f1063fd339 chore: release tokens 2026-03-29 01:13:13 +00:00
simosmik
27cd12432b chore: relicense to AGPL-3.0-or-later
Siteboon AI B.V. contributions relicensed from GPL-3.0 to
AGPL-3.0-or-later. Existing community contributions remain
under GPL-3.0, combined per GPL-3.0 Section 13.
Adds Section 7 additional terms (attribution, origin
protection, publicity restriction, trademark).
2026-03-29 00:57:09 +00:00
simosmik
004135ef01 chore: add terminal plugin in the plugins list
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:38:00 +00:00
xiguatoutou
b54cdf8168 fix: prevent split on undefined(#491) (#563) 2026-03-23 20:14:15 +03:00
simosmik
42a131389a chore: add release-it github action 2026-03-22 01:41:21 +00:00
simosmik
ebd1c0db92 chore(release): v1.26.3 2026-03-22 01:10:13 +00:00
simosmik
6d87cc5566 chore(release): v1.26.2 2026-03-21 16:59:38 +00:00
simosmik
17d6ec54af fix: change SW cache mechanism 2026-03-21 16:49:56 +00:00
simosmik
a41d2c713e fix: claude auth changes and adding copy on mobile 2026-03-21 16:40:44 +00:00
41 changed files with 1389 additions and 649 deletions

50
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Release
on:
workflow_dispatch:
inputs:
increment:
description: 'Version bump: patch, minor, major, or explicit (e.g. 1.27.0)'
required: true
default: 'patch'
type: string
release_name:
description: 'Custom release name (optional, defaults to "CloudCLI UI vX.Y.Z")'
required: false
type: string
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_PAT }}
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- name: git config
run: |
git config user.name "${GITHUB_ACTOR}"
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
- run: npm ci
- name: Release
run: |
ARGS="--ci --increment=${{ inputs.increment }}"
if [ -n "${{ inputs.release_name }}" ]; then
ARGS="$ARGS --github.releaseName=\"${{ inputs.release_name }}\""
fi
npx release-it $ARGS
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -6,7 +6,8 @@
"requireCleanWorkingDir": true
},
"npm": {
"publish": true
"publish": true,
"publishArgs": ["--access public"]
},
"github": {
"release": true,

View File

@@ -3,6 +3,39 @@
All notable changes to CloudCLI UI will be documented in this file.
## [1.28.0](https://github.com/siteboon/claudecodeui/compare/v1.27.1...v1.28.0) (2026-04-03)
### New Features
* adding session resume in the api ([8f1042c](https://github.com/siteboon/claudecodeui/commit/8f1042cf256be282f009adcceeb55ab2dddf3fba))
* moving new session button higher ([1628868](https://github.com/siteboon/claudecodeui/commit/16288684702dec894cf054291ca3d545ddb8214b))
### Maintenance
* changing package name to @cloudcli-ai/cloudcli ([ef51de2](https://github.com/siteboon/claudecodeui/commit/ef51de259ea2b963bc15f058b084e11220bc216a))
## [1.27.1](https://github.com/siteboon/claudecodeui/compare/v1.26.3...v1.27.1) (2026-03-29)
### Bug Fixes
* prevent split on undefined[#491](https://github.com/siteboon/claudecodeui/issues/491) ([#563](https://github.com/siteboon/claudecodeui/issues/563)) ([b54cdf8](https://github.com/siteboon/claudecodeui/commit/b54cdf8168fc224e9907796e4229ae8ed34e6885))
### Maintenance
* add release-it github action ([42a1313](https://github.com/siteboon/claudecodeui/commit/42a131389a6954df0d2c3bedd2cb6d3406c5ebc1))
* add terminal plugin in the plugins list ([004135e](https://github.com/siteboon/claudecodeui/commit/004135ef0187023e1da29c4a7137a28a42ebf9af))
* release tokens ([f1063fd](https://github.com/siteboon/claudecodeui/commit/f1063fd33964ccb517f5ebcdd14526ed162e1138))
* relicense to AGPL-3.0-or-later ([27cd124](https://github.com/siteboon/claudecodeui/commit/27cd12432b7d3237981f86acd9cc99532d843d4a))
## [1.26.3](https://github.com/siteboon/claudecodeui/compare/v1.26.2...v1.26.3) (2026-03-22)
## [1.26.2](https://github.com/siteboon/claudecodeui/compare/v1.26.0...v1.26.2) (2026-03-21)
### Bug Fixes
* change SW cache mechanism ([17d6ec5](https://github.com/siteboon/claudecodeui/commit/17d6ec54af18d333c8b04d2ffc64793e688d996e))
* claude auth changes and adding copy on mobile ([a41d2c7](https://github.com/siteboon/claudecodeui/commit/a41d2c713e87d56f23d5884585b4bb43c43a250a))
## [1.26.0](https://github.com/siteboon/claudecodeui/compare/v1.25.2...v1.26.0) (2026-03-20)
### New Features

View File

@@ -153,4 +153,4 @@ This automatically:
## License
By contributing, you agree that your contributions will be licensed under the [GPL-3.0 License](LICENSE).
By contributing, you agree that your contributions will be licensed under the [AGPL-3.0-or-later License](LICENSE), including the additional terms specified in Section 7 of the LICENSE file.

789
LICENSE

File diff suppressed because it is too large Load Diff

13
NOTICE Normal file
View File

@@ -0,0 +1,13 @@
CloudCLI UI
Copyright 2025-2026 Siteboon AI B.V. and contributors
This software is licensed under the GNU Affero General Public License v3.0
or later (AGPL-3.0-or-later). See the LICENSE file for the full license text,
including additional terms under Section 7.
Originally developed by Siteboon AI B.V. (https://github.com/siteboon/claudecodeui).
Contributions by Siteboon AI B.V. prior to commit 004135ef were originally
published under GPL-3.0 and are hereby relicensed to AGPL-3.0-or-later.
Contributions by other authors prior to that commit remain under GPL-3.0
and are incorporated into this work as permitted by GPL-3.0 Section 13.

View File

@@ -79,13 +79,13 @@ Der schnellste Einstieg keine lokale Einrichtung erforderlich. Erhalte eine
CloudCLI UI sofort mit **npx** ausprobieren (erfordert **Node.js** v22+):
```bash
npx @siteboon/claude-code-ui
npx @cloudcli-ai/cloudcli
```
Oder **global** installieren für regelmäßige Nutzung:
```bash
npm install -g @siteboon/claude-code-ui
npm install -g @cloudcli-ai/cloudcli
cloudcli
```
@@ -104,7 +104,7 @@ CloudCLI UI ist die Open-Source-UI-Schicht, die CloudCLI Cloud antreibt. Du kann
|---|---|---|
| **Am besten für** | Entwickler:innen, die eine vollständige UI für lokale Agent-Sitzungen auf ihrem eigenen Rechner möchten | Teams und Entwickler:innen, die Agents in der Cloud betreiben möchten, überall erreichbar |
| **Zugriff** | Browser via `[deineIP]:port` | Browser, jede IDE, REST API, n8n |
| **Einrichtung** | `npx @siteboon/claude-code-ui` | Keine Einrichtung erforderlich |
| **Einrichtung** | `npx @cloudcli-ai/cloudcli` | Keine Einrichtung erforderlich |
| **Rechner muss laufen** | Ja | Nein |
| **Mobiler Zugriff** | Jeder Browser im Netzwerk | Jedes Gerät, native App in Entwicklung |
| **Verfügbare Sitzungen** | Alle Sitzungen automatisch aus `~/.claude` erkannt | Alle Sitzungen in deiner Cloud-Umgebung |

View File

@@ -75,13 +75,13 @@
**npx** で今すぐ CloudCLI UI を試せます(**Node.js** v22+ が必要):
```bash
npx @siteboon/claude-code-ui
npx @cloudcli-ai/cloudcli
```
または、普段使いするなら **グローバル** にインストール:
```bash
npm install -g @siteboon/claude-code-ui
npm install -g @cloudcli-ai/cloudcli
cloudcli
```
@@ -100,7 +100,7 @@ CloudCLI UI は、CloudCLI Cloud を支えるオープンソースの UI レイ
|---|---|---|
| **対象ユーザー** | 自分のマシン上でローカルの agent セッションに対してフル UI を使いたい開発者 | クラウド上で動く agents をどこからでも利用したいチーム/開発者 |
| **アクセス方法** | ブラウザ(`[yourip]:port` | ブラウザ、任意の IDE、REST API、n8n |
| **セットアップ** | `npx @siteboon/claude-code-ui` | セットアップ不要 |
| **セットアップ** | `npx @cloudcli-ai/cloudcli` | セットアップ不要 |
| **マシンの稼働継続** | はい | いいえ |
| **モバイルアクセス** | 同一ネットワーク内の任意のブラウザ | 任意のデバイス(ネイティブアプリも準備中) |
| **利用可能なセッション** | `~/.claude` から全セッションを自動検出 | クラウド環境内の全セッション |

View File

@@ -75,13 +75,13 @@
**npx**로 즉시 CloudCLI UI를 실행하세요 (Node.js v22+ 필요):
```bash
npx @siteboon/claude-code-ui
npx @cloudcli-ai/cloudcli
```
**정기적으로 사용한다면 전역 설치:**
```bash
npm install -g @siteboon/claude-code-ui
npm install -g @cloudcli-ai/cloudcli
cloudcli
```
@@ -99,7 +99,7 @@ CloudCLI UI는 CloudCLI Cloud를 구동하는 오픈 소스 UI 계층입니다.
|---|---|---|
| **적합한 대상** | 로컬 에이전트 세션을 위한 전체 UI가 필요한 개발자 | 어디서든 접근 가능한 클라우드에서 에이전트를 운영하고자 하는 팀 및 개발자 |
| **접근 방법** | `[yourip]:port`를 통해 브라우저 접속 | 브라우저, IDE, REST API, n8n |
| **설정** | `npx @siteboon/claude-code-ui` | 설정 불필요 |
| **설정** | `npx @cloudcli-ai/cloudcli` | 설정 불필요 |
| **기기 유지 필요 여부** | 예 (머신 켜둬야 함) | 아니오 |
| **모바일 접근** | 네트워크 내 브라우저 | 모든 기기 (네이티브 앱 예정) |
| **세션 접근** | `~/.claude`에서 자동 발견 | 클라우드 환경 내 세션 |

View File

@@ -79,13 +79,13 @@ The fastest way to get started — no local setup required. Get a fully managed,
Try CloudCLI UI instantly with **npx** (requires **Node.js** v22+):
```
npx @siteboon/claude-code-ui
npx @cloudcli-ai/cloudcli
```
Or install **globally** for regular use:
```
npm install -g @siteboon/claude-code-ui
npm install -g @cloudcli-ai/cloudcli
cloudcli
```
@@ -104,7 +104,7 @@ CloudCLI UI is the open source UI layer that powers CloudCLI Cloud. You can self
|---|---|---|
| **Best for** | Developers who want a full UI for local agent sessions on their own machine | Teams and developers who want agents running in the cloud, accessible from anywhere |
| **How you access it** | Browser via `[yourip]:port` | Browser, any IDE, REST API, n8n |
| **Setup** | `npx @siteboon/claude-code-ui` | No setup required |
| **Setup** | `npx @cloudcli-ai/cloudcli` | No setup required |
| **Machine needs to stay on** | Yes | No |
| **Mobile access** | Any browser on your network | Any device, native app coming |
| **Sessions available** | All sessions auto-discovered from `~/.claude` | All sessions within your cloud environment |
@@ -213,9 +213,11 @@ Yes, for self-hosted. CloudCLI UI reads from and writes to the same `~/.claude`
## License
GNU General Public License v3.0 - see [LICENSE](LICENSE) file for details.
GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) — see [LICENSE](LICENSE) for the full text, including additional terms under Section 7.
This project is open source and free to use, modify, and distribute under the GPL v3 license.
This project is open source and free to use, modify, and distribute under the AGPL-3.0-or-later license. If you modify this software and run it as a network service, you must make your modified source code available to users of that service.
CloudCLI UI - (https://cloudcli.ai).
## Acknowledgments

View File

@@ -79,13 +79,13 @@
Попробовать CloudCLI UI можно сразу через **npx** (требуется **Node.js** v22+):
```bash
npx @siteboon/claude-code-ui
npx @cloudcli-ai/cloudcli
```
Или установить **глобально** для регулярного использования:
```bash
npm install -g @siteboon/claude-code-ui
npm install -g @cloudcli-ai/cloudcli
cloudcli
```
@@ -104,7 +104,7 @@ CloudCLI UI — это open source UI-слой, на котором постро
|---|---|---|
| **Лучше всего подходит для** | Разработчиков, которым нужен полноценный UI для локальных агентских сессий на своей машине | Команд и разработчиков, которым нужны агенты в облаке с доступом откуда угодно |
| **Как вы получаете доступ** | Браузер через `[yourip]:port` | Браузер, любая IDE, REST API, n8n |
| **Настройка** | `npx @siteboon/claude-code-ui` | Настройка не требуется |
| **Настройка** | `npx @cloudcli-ai/cloudcli` | Настройка не требуется |
| **Машина должна оставаться включённой** | Да | Нет |
| **Доступ с мобильных устройств** | Любой браузер в вашей сети | Любое устройство, нативное приложение в разработке |
| **Доступные сессии** | Все сессии автоматически обнаруживаются из `~/.claude` | Все сессии внутри вашей облачной среды |

View File

@@ -75,13 +75,13 @@
启动 CloudCLI UI只需一行 `npx`(需要 Node.js v22+
```bash
npx @siteboon/claude-code-ui
npx @cloudcli-ai/cloudcli
```
或进行全局安装,便于日常使用:
```bash
npm install -g @siteboon/claude-code-ui
npm install -g @cloudcli-ai/cloudcli
cloudcli
```
@@ -99,7 +99,7 @@ CloudCLI UI 是 CloudCLI Cloud 的开源 UI 层。你可以在本地机器上自
|---|---|---|
| **适合对象** | 需要为本地代理会话提供完整 UI 的开发者 | 需要部署在云端,随时从任何地方访问代理的团队与开发者 |
| **访问方式** | 通过 `[yourip]:port` 在浏览器中访问 | 浏览器、任意 IDE、REST API、n8n |
| **设置** | `npx @siteboon/claude-code-ui` | 无需设置 |
| **设置** | `npx @cloudcli-ai/cloudcli` | 无需设置 |
| **机器需保持开机吗** | 是 | 否 |
| **移动端访问** | 网络内任意浏览器 | 任意设备(原生应用即将推出) |
| **可用会话** | 自动发现 `~/.claude` 中的所有会话 | 云端环境内的会话 |

89
docker/README.md Normal file
View File

@@ -0,0 +1,89 @@
# Claude Code UI — Docker Sandbox Templates
Run AI coding agents with a full web IDE inside [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/).
Instead of a terminal-only experience, get a browser-based interface with chat, file explorer, git panel, shell, and MCP configuration — all running safely inside an isolated sandbox.
## Available Templates
| Template | Base Image | Agent |
|----------|-----------|-------|
| `cloudcli-ai/sandbox:claude-code` | `docker/sandbox-templates:claude-code` | Claude Code |
| `cloudcli-ai/sandbox:codex` | `docker/sandbox-templates:codex` | OpenAI Codex |
| `cloudcli-ai/sandbox:gemini` | `docker/sandbox-templates:gemini` | Gemini CLI |
## Quick Start
### 1. Start a sandbox with the template
```bash
sbx run --template docker.io/cloudcli-ai/sandbox:claude-code claude ~/my-project
```
### 2. Forward the UI port
```bash
sbx ports <sandbox-name> --publish 3001:3001
```
### 3. Open the browser
```
http://localhost:3001
```
On first visit you'll set a password — this protects the UI if the port is ever exposed beyond localhost.
## What You Get
- **Chat** — Rich conversation UI with markdown rendering, code blocks, and message history
- **Files** — Visual file tree with syntax-highlighted editor
- **Git** — Diff viewer, staging, branch switching, and commit — all visual
- **Shell** — Built-in terminal emulator
- **MCP** — Configure Model Context Protocol servers through the UI
- **Mobile** — Works on tablet and phone browsers
## Building Locally
All Dockerfiles share scripts from `shared/`. Build with the `docker/` directory as context:
```bash
# Claude Code variant
docker build -f docker/claude-code/Dockerfile -t cloudcli-sandbox:claude-code docker/
# Codex variant
docker build -f docker/codex/Dockerfile -t cloudcli-sandbox:codex docker/
# Gemini variant
docker build -f docker/gemini/Dockerfile -t cloudcli-sandbox:gemini docker/
```
## How It Works
Each template extends Docker's official sandbox base image and adds:
1. **Node.js 22** — Runtime for Claude Code UI
2. **Claude Code UI** — Installed globally via `npm install -g @cloudcli-ai/cloudcli`
3. **Auto-start** — The UI server starts in the background when the sandbox shell opens (port 3001)
The agent (Claude Code, Codex, or Gemini) comes from the base image. Claude Code UI connects to it and provides the web interface on top.
## Configuration
| Environment Variable | Default | Description |
|---------------------|---------|-------------|
| `SERVER_PORT` | `3001` | Port for the web UI |
| `HOST` | `0.0.0.0` | Bind address |
| `DATABASE_PATH` | `~/.cloudcli/auth.db` | SQLite database location |
## Network Policies
If your sandbox uses restricted network policies, allow the UI port:
```bash
sbx policy allow network "localhost:3001"
```
## License
These templates are free and open-source under the same license as Claude Code UI (AGPL-3.0-or-later).

View File

@@ -0,0 +1,11 @@
FROM docker/sandbox-templates:claude-code
USER root
COPY shared/install-cloudcli.sh /tmp/install-cloudcli.sh
RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh
USER agent
RUN npm install -g @cloudcli-ai/cloudcli
COPY shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh
RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc

11
docker/codex/Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM docker/sandbox-templates:codex
USER root
COPY shared/install-cloudcli.sh /tmp/install-cloudcli.sh
RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh
USER agent
RUN npm install -g @cloudcli-ai/cloudcli
COPY shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh
RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc

11
docker/gemini/Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM docker/sandbox-templates:gemini
USER root
COPY shared/install-cloudcli.sh /tmp/install-cloudcli.sh
RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh
USER agent
RUN npm install -g @cloudcli-ai/cloudcli
COPY shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh
RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc

View File

@@ -0,0 +1,14 @@
#!/bin/bash
set -e
# Install Node.js 22 LTS
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
# Install Node.js + build tools needed for native modules (node-pty, better-sqlite3, bcrypt)
# Node.js + build tools for native modules + common dev tools
apt-get install -y --no-install-recommends \
nodejs build-essential python3 python3-setuptools \
jq ripgrep sqlite3 zip unzip tree vim-tiny
# Clean up apt cache to reduce image size
rm -rf /var/lib/apt/lists/*

View File

@@ -0,0 +1,23 @@
#!/bin/bash
# Auto-start Claude Code UI server in background if not already running.
# This script is sourced from ~/.bashrc on sandbox shell open.
if ! pgrep -f "server/index.js" > /dev/null 2>&1; then
# Start the pre-installed version immediately
nohup cloudcli start --port 3001 > /tmp/cloudcli-ui.log 2>&1 &
disown
# Check for updates in the background (non-blocking)
nohup npm update -g @cloudcli-ai/cloudcli > /tmp/cloudcli-update.log 2>&1 &
disown
echo ""
echo " Claude Code UI is starting on port 3001..."
echo ""
echo " To access the web UI, forward the port:"
echo " sbx ports \$(hostname) --publish 3001:3001"
echo ""
echo " Then open: http://localhost:3001"
echo ""
fi

11
package-lock.json generated
View File

@@ -1,14 +1,14 @@
{
"name": "@siteboon/claude-code-ui",
"version": "1.26.0",
"name": "@cloudcli-ai/cloudcli",
"version": "1.28.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@siteboon/claude-code-ui",
"version": "1.26.0",
"name": "@cloudcli-ai/cloudcli",
"version": "1.28.0",
"hasInstallScript": true,
"license": "GPL-3.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.59",
"@codemirror/lang-css": "^6.3.1",
@@ -69,7 +69,6 @@
"ws": "^8.14.2"
},
"bin": {
"claude-code-ui": "server/cli.js",
"cloudcli": "server/cli.js"
},
"devDependencies": {

View File

@@ -1,11 +1,10 @@
{
"name": "@siteboon/claude-code-ui",
"version": "1.26.0",
"name": "@cloudcli-ai/cloudcli",
"version": "1.28.0",
"description": "A web-based UI for Claude Code CLI",
"type": "module",
"main": "server/index.js",
"bin": {
"claude-code-ui": "server/cli.js",
"cloudcli": "server/cli.js"
},
"files": [
@@ -40,13 +39,24 @@
},
"keywords": [
"claude code",
"ai",
"claude-code",
"claude-code-ui",
"cloudcli",
"codex",
"gemini",
"gemini-cli",
"cursor",
"cursor-cli",
"anthropic",
"openai",
"google",
"coding-agent",
"web-ui",
"ui",
"mobile"
"mobile IDE"
],
"author": "CloudCLI UI Contributors",
"license": "GPL-3.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.59",
"@codemirror/lang-css": "^6.3.1",

View File

@@ -1,8 +1,8 @@
// Service Worker for Claude Code UI PWA
const CACHE_NAME = 'claude-ui-v1';
// Cache only manifest (needed for PWA install). HTML and JS are never pre-cached
// so a rebuild + refresh always picks up the latest assets.
const CACHE_NAME = 'claude-ui-v2';
const urlsToCache = [
'/',
'/index.html',
'/manifest.json'
];
@@ -10,44 +10,63 @@ const urlsToCache = [
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(urlsToCache);
})
.then(cache => cache.addAll(urlsToCache))
);
self.skipWaiting();
});
// Fetch event
// Fetch event — network-first for everything except hashed assets
self.addEventListener('fetch', event => {
// Never cache API requests or WebSocket upgrades
if (event.request.url.includes('/api/') || event.request.url.includes('/ws')) {
const url = event.request.url;
// Never intercept API requests or WebSocket upgrades
if (url.includes('/api/') || url.includes('/ws')) {
return;
}
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
// Navigation requests (HTML) — always go to network, no caching
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => caches.match('/manifest.json').then(() =>
new Response('<h1>Offline</h1><p>Please check your connection.</p>', {
headers: { 'Content-Type': 'text/html' }
})
))
);
return;
}
// Hashed assets (JS/CSS in /assets/) — cache-first since filenames change per build
if (url.includes('/assets/')) {
event.respondWith(
caches.match(event.request).then(cached => {
if (cached) return cached;
return fetch(event.request).then(response => {
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
return response;
}
return fetch(event.request);
}
)
});
})
);
return;
}
// Everything else — network-first
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
);
});
// Activate event
// Activate event — purge old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
caches.keys().then(cacheNames =>
Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
)
)
);
self.clients.claim();
});

248
redirect-package/README.md Normal file
View File

@@ -0,0 +1,248 @@
<div align="center">
> ## This package has moved to [`@cloudcli-ai/cloudcli`](https://www.npmjs.com/package/@cloudcli-ai/cloudcli)
>
> ```bash
> npm install -g @cloudcli-ai/cloudcli
> ```
>
> This package (`@siteboon/claude-code-ui`) is now a thin wrapper that installs the new package automatically.
> For new installations, use `@cloudcli-ai/cloudcli` directly.
</div>
---
<div align="center">
<img src="https://raw.githubusercontent.com/siteboon/claudecodeui/main/public/logo.svg" alt="CloudCLI UI" width="64" height="64">
<h1>Cloud CLI (aka Claude Code UI)</h1>
<p>A desktop and mobile UI for <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a>, <a href="https://docs.cursor.com/en/cli/overview">Cursor CLI</a>, <a href="https://developers.openai.com/codex">Codex</a>, and <a href="https://geminicli.com/">Gemini-CLI</a>.<br>Use it locally or remotely to view your active projects and sessions from everywhere.</p>
</div>
<p align="center">
<a href="https://cloudcli.ai">CloudCLI Cloud</a> · <a href="https://cloudcli.ai/docs">Documentation</a> · <a href="https://discord.gg/buxwujPNRE">Discord</a> · <a href="https://github.com/siteboon/claudecodeui/issues">Bug Reports</a> · <a href="https://github.com/siteboon/claudecodeui/blob/main/CONTRIBUTING.md">Contributing</a>
</p>
<p align="center">
<a href="https://cloudcli.ai"><img src="https://img.shields.io/badge/☁_CloudCLI_Cloud-Try_Now-0066FF?style=for-the-badge" alt="CloudCLI Cloud"></a>
<a href="https://discord.gg/buxwujPNRE"><img src="https://img.shields.io/badge/Discord-Join%20Community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join our Discord"></a>
<br><br>
<a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p>
---
## Screenshots
<div align="center">
<table>
<tr>
<td align="center">
<h3>Desktop View</h3>
<img src="https://raw.githubusercontent.com/siteboon/claudecodeui/main/public/screenshots/desktop-main.png" alt="Desktop Interface" width="400">
<br>
<em>Main interface showing project overview and chat</em>
</td>
<td align="center">
<h3>Mobile Experience</h3>
<img src="https://raw.githubusercontent.com/siteboon/claudecodeui/main/public/screenshots/mobile-chat.png" alt="Mobile Interface" width="250">
<br>
<em>Responsive mobile design with touch navigation</em>
</td>
</tr>
<tr>
<td align="center" colspan="2">
<h3>CLI Selection</h3>
<img src="https://raw.githubusercontent.com/siteboon/claudecodeui/main/public/screenshots/cli-selection.png" alt="CLI Selection" width="400">
<br>
<em>Select between Claude Code, Gemini, Cursor CLI and Codex</em>
</td>
</tr>
</table>
</div>
## Features
- **Responsive Design** - Works seamlessly across desktop, tablet, and mobile so you can also use Agents from mobile
- **Interactive Chat Interface** - Built-in chat interface for seamless communication with the Agents
- **Integrated Shell Terminal** - Direct access to the Agents CLI through built-in shell functionality
- **File Explorer** - Interactive file tree with syntax highlighting and live editing
- **Git Explorer** - View, stage and commit your changes. You can also switch branches
- **Session Management** - Resume conversations, manage multiple sessions, and track history
- **Plugin System** - Extend CloudCLI with custom plugins — add new tabs, backend services, and integrations. [Build your own →](https://github.com/cloudcli-ai/cloudcli-plugin-starter)
- **TaskMaster AI Integration** *(Optional)* - Advanced project management with AI-powered task planning, PRD parsing, and workflow automation
- **Model Compatibility** - Works with Claude, GPT, and Gemini model families (see [`shared/modelConstants.js`](https://github.com/siteboon/claudecodeui/blob/main/shared/modelConstants.js) for the full list of supported models)
## Quick Start
### CloudCLI Cloud (Recommended)
The fastest way to get started — no local setup required. Get a fully managed, containerized development environment accessible from the web, mobile app, API, or your favorite IDE.
**[Get started with CloudCLI Cloud](https://cloudcli.ai)**
### Self-Hosted (Open source)
Try CloudCLI UI instantly with **npx** (requires **Node.js** v22+):
```
npx @cloudcli-ai/cloudcli
```
Or install **globally** for regular use:
```
npm install -g @cloudcli-ai/cloudcli
cloudcli
```
Open `http://localhost:3001` — all your existing sessions are discovered automatically.
Visit the **[documentation →](https://cloudcli.ai/docs)** for more full configuration options, PM2, remote server setup and more
---
## Which option is right for you?
CloudCLI UI is the open source UI layer that powers CloudCLI Cloud. You can self-host it on your own machine, or use CloudCLI Cloud which builds on top of it with a full managed cloud environment, team features, and deeper integrations.
| | CloudCLI UI (Self-hosted) | CloudCLI Cloud |
|---|---|---|
| **Best for** | Developers who want a full UI for local agent sessions on their own machine | Teams and developers who want agents running in the cloud, accessible from anywhere |
| **How you access it** | Browser via `[yourip]:port` | Browser, any IDE, REST API, n8n |
| **Setup** | `npx @cloudcli-ai/cloudcli` | No setup required |
| **Machine needs to stay on** | Yes | No |
| **Mobile access** | Any browser on your network | Any device, native app coming |
| **Sessions available** | All sessions auto-discovered from `~/.claude` | All sessions within your cloud environment |
| **Agents supported** | Claude Code, Cursor CLI, Codex, Gemini CLI | Claude Code, Cursor CLI, Codex, Gemini CLI |
| **File explorer and Git** | Yes, built into the UI | Yes, built into the UI |
| **MCP configuration** | Managed via UI, synced with your local `~/.claude` config | Managed via UI |
| **IDE access** | Your local IDE | Any IDE connected to your cloud environment |
| **REST API** | Yes | Yes |
| **n8n node** | No | Yes |
| **Team sharing** | No | Yes |
| **Platform cost** | Free, open source | Starts at $7/month |
> Both options use your own AI subscriptions (Claude, Cursor, etc.) — CloudCLI provides the environment, not the AI.
---
## Security & Tools Configuration
**Important Notice**: All Claude Code tools are **disabled by default**. This prevents potentially harmful operations from running automatically.
### Enabling Tools
To use Claude Code's full functionality, you'll need to manually enable tools:
1. **Open Tools Settings** - Click the gear icon in the sidebar
2. **Enable Selectively** - Turn on only the tools you need
3. **Apply Settings** - Your preferences are saved locally
**Recommended approach**: Start with basic tools enabled and add more as needed. You can always adjust these settings later.
---
## Plugins
CloudCLI has a plugin system that lets you add custom tabs with their own frontend UI and optional Node.js backend. Install plugins from git repos directly in **Settings > Plugins**, or build your own.
### Available Plugins
| Plugin | Description |
|---|---|
| **[Project Stats](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** | Shows file counts, lines of code, file-type breakdown, largest files, and recently modified files for your current project |
| **[Web Terminal](https://github.com/cloudcli-ai/cloudcli-plugin-terminal)** | Full xterm.js terminal with multi-tab support|
### Build Your Own
**[Plugin Starter Template →](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** — fork this repo to create your own plugin. It includes a working example with frontend rendering, live context updates, and RPC communication to a backend server.
**[Plugin Documentation →](https://cloudcli.ai/docs/plugin-overview)** — full guide to the plugin API, manifest format, security model, and more.
---
## FAQ
<details>
<summary>How is this different from Claude Code Remote Control?</summary>
Claude Code Remote Control lets you send messages to a session already running in your local terminal. Your machine has to stay on, your terminal has to stay open, and sessions time out after roughly 10 minutes without a network connection.
CloudCLI UI and CloudCLI Cloud extend Claude Code rather than sit alongside it — your MCP servers, permissions, settings, and sessions are the exact same ones Claude Code uses natively. Nothing is duplicated or managed separately.
Here's what that means in practice:
- **All your sessions, not just one** — CloudCLI UI auto-discovers every session from your `~/.claude` folder. Remote Control only exposes the single active session to make it available in the Claude mobile app.
- **Your settings are your settings** — MCP servers, tool permissions, and project config you change in CloudCLI UI are written directly to your Claude Code config and take effect immediately, and vice versa.
- **Works with more agents** — Claude Code, Cursor CLI, Codex, and Gemini CLI, not just Claude Code.
- **Full UI, not just a chat window** — file explorer, Git integration, MCP management, and a shell terminal are all built in.
- **CloudCLI Cloud runs in the cloud** — close your laptop, the agent keeps running. No terminal to babysit, no machine to keep awake.
</details>
<details>
<summary>Do I need to pay for an AI subscription separately?</summary>
Yes. CloudCLI provides the environment, not the AI. You bring your own Claude, Cursor, Codex, or Gemini subscription. CloudCLI Cloud starts at $7/month for the hosted environment on top of that.
</details>
<details>
<summary>Can I use CloudCLI UI on my phone?</summary>
Yes. For self-hosted, run the server on your machine and open `[yourip]:port` in any browser on your network. For CloudCLI Cloud, open it from any device — no VPN, no port forwarding, no setup. A native app is also in the works.
</details>
<details>
<summary>Will changes I make in the UI affect my local Claude Code setup?</summary>
Yes, for self-hosted. CloudCLI UI reads from and writes to the same `~/.claude` config that Claude Code uses natively. MCP servers you add via the UI show up in Claude Code immediately and vice versa.
</details>
---
## Community & Support
- **[Documentation](https://cloudcli.ai/docs)** — installation, configuration, features, and troubleshooting
- **[Discord](https://discord.gg/buxwujPNRE)** — get help and connect with other users
- **[GitHub Issues](https://github.com/siteboon/claudecodeui/issues)** — bug reports and feature requests
- **[Contributing Guide](https://github.com/siteboon/claudecodeui/blob/main/CONTRIBUTING.md)** — how to contribute to the project
## License
GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) — see [LICENSE](https://github.com/siteboon/claudecodeui/blob/main/LICENSE) for the full text, including additional terms under Section 7.
This project is open source and free to use, modify, and distribute under the AGPL-3.0-or-later license. If you modify this software and run it as a network service, you must make your modified source code available to users of that service.
CloudCLI UI - (https://cloudcli.ai).
## Acknowledgments
### Built With
- **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** - Anthropic's official CLI
- **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** - Cursor's official CLI
- **[Codex](https://developers.openai.com/codex)** - OpenAI Codex
- **[Gemini-CLI](https://geminicli.com/)** - Google Gemini CLI
- **[React](https://react.dev/)** - User interface library
- **[Vite](https://vitejs.dev/)** - Fast build tool and dev server
- **[Tailwind CSS](https://tailwindcss.com/)** - Utility-first CSS framework
- **[CodeMirror](https://codemirror.net/)** - Advanced code editor
- **[TaskMaster AI](https://github.com/eyaltoledano/claude-task-master)** *(Optional)* - AI-powered project management and task planning
### Sponsors
- [Siteboon - AI powered website builder](https://siteboon.ai)
---
<div align="center">
<strong>Made with care for the Claude Code, Cursor and Codex community.</strong>
</div>

2
redirect-package/bin.js Normal file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
import('@cloudcli-ai/cloudcli/server/cli.js');

View File

@@ -0,0 +1,2 @@
export * from '@cloudcli-ai/cloudcli';
export { default } from '@cloudcli-ai/cloudcli';

View File

@@ -0,0 +1,43 @@
{
"name": "@siteboon/claude-code-ui",
"version": "2.0.0",
"description": "This package has moved to @cloudcli-ai/cloudcli",
"type": "module",
"main": "index.js",
"bin": {
"claude-code-ui": "./bin.js",
"cloudcli": "./bin.js"
},
"homepage": "https://cloudcli.ai",
"repository": {
"type": "git",
"url": "git+https://github.com/siteboon/claudecodeui.git"
},
"bugs": {
"url": "https://github.com/siteboon/claudecodeui/issues"
},
"keywords": [
"claude code",
"claude-code",
"claude-code-ui",
"cloudcli",
"codex",
"gemini",
"gemini-cli",
"cursor",
"cursor-cli",
"anthropic",
"openai",
"google",
"coding-agent",
"web-ui",
"ui",
"mobile IDE"
],
"author": "CloudCLI UI Contributors",
"dependencies": {
"@cloudcli-ai/cloudcli": "*"
},
"deprecated": "This package has been renamed to @cloudcli-ai/cloudcli. Please install @cloudcli-ai/cloudcli instead.",
"license": "AGPL-3.0-or-later"
}

View File

@@ -203,7 +203,7 @@ function isNewerVersion(v1, v2) {
async function checkForUpdates(silent = false) {
try {
const { execSync } = await import('child_process');
const latestVersion = execSync('npm show @siteboon/claude-code-ui version', { encoding: 'utf8' }).trim();
const latestVersion = execSync('npm show @cloudcli-ai/cloudcli version', { encoding: 'utf8' }).trim();
const currentVersion = packageJson.version;
if (isNewerVersion(latestVersion, currentVersion)) {
@@ -236,11 +236,11 @@ async function updatePackage() {
}
console.log(`${c.info('[INFO]')} Updating from ${currentVersion} to ${latestVersion}...`);
execSync('npm update -g @siteboon/claude-code-ui', { stdio: 'inherit' });
execSync('npm update -g @cloudcli-ai/cloudcli', { stdio: 'inherit' });
console.log(`${c.ok('[OK]')} Update complete! Restart cloudcli to use the new version.`);
} catch (e) {
console.error(`${c.error('[ERROR]')} Update failed: ${e.message}`);
console.log(`${c.tip('[TIP]')} Try running manually: npm update -g @siteboon/claude-code-ui`);
console.log(`${c.tip('[TIP]')} Try running manually: npm update -g @cloudcli-ai/cloudcli`);
}
}

View File

@@ -438,7 +438,7 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
// Run the update command based on install mode
const updateCommand = installMode === 'git'
? 'git checkout main && git pull && npm install'
: 'npm install -g @siteboon/claude-code-ui@latest';
: 'npm install -g @cloudcli-ai/cloudcli@latest';
const child = spawn('sh', ['-c', updateCommand], {
cwd: installMode === 'git' ? projectRoot : os.homedir(),

View File

@@ -475,6 +475,7 @@ class SSEStreamWriter {
setSessionId(sessionId) {
this.sessionId = sessionId;
this.send({ type: 'session-id', sessionId });
}
getSessionId() {
@@ -839,7 +840,7 @@ class ResponseCollector {
* }
*/
router.post('/', validateExternalApiKey, async (req, res) => {
const { githubUrl, projectPath, message, provider = 'claude', model, githubToken, branchName } = req.body;
const { githubUrl, projectPath, message, provider = 'claude', model, githubToken, branchName, sessionId } = req.body;
// Parse stream and cleanup as booleans (handle string "true"/"false" from curl)
const stream = req.body.stream === undefined ? true : (req.body.stream === true || req.body.stream === 'true');
@@ -949,7 +950,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
await queryClaudeSDK(message.trim(), {
projectPath: finalProjectPath,
cwd: finalProjectPath,
sessionId: null, // New session
sessionId: sessionId || null,
model: model,
permissionMode: 'bypassPermissions' // Bypass all permissions for API calls
}, writer);
@@ -960,7 +961,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
await spawnCursor(message.trim(), {
projectPath: finalProjectPath,
cwd: finalProjectPath,
sessionId: null, // New session
sessionId: sessionId || null,
model: model || undefined,
skipPermissions: true // Bypass permissions for Cursor
}, writer);
@@ -970,7 +971,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
await queryCodex(message.trim(), {
projectPath: finalProjectPath,
cwd: finalProjectPath,
sessionId: null,
sessionId: sessionId || null,
model: model || CODEX_MODELS.DEFAULT,
permissionMode: 'bypassPermissions'
}, writer);
@@ -980,7 +981,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
await spawnGemini(message.trim(), {
projectPath: finalProjectPath,
cwd: finalProjectPath,
sessionId: null,
sessionId: sessionId || null,
model: model,
skipPermissions: true // CLI mode bypasses permissions
}, writer);

View File

@@ -33,7 +33,12 @@ export const ToolDiffViewer: React.FC<ToolDiffViewerProps> = ({
: 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400';
const diffLines = useMemo(
() => createDiff(oldContent, newContent),
() => {
if (oldContent === undefined || newContent === undefined) {
return [];
}
return createDiff(oldContent, newContent)
},
[createDiff, oldContent, newContent]
);

View File

@@ -6,6 +6,7 @@ import type { Plugin } from '../../../contexts/PluginsContext';
import PluginIcon from './PluginIcon';
const STARTER_PLUGIN_URL = 'https://github.com/cloudcli-ai/cloudcli-plugin-starter';
const TERMINAL_PLUGIN_URL = 'https://github.com/cloudcli-ai/cloudcli-plugin-terminal';
/* ─── Toggle Switch ─────────────────────────────────────────────────────── */
function ToggleSwitch({ checked, onChange, ariaLabel }: { checked: boolean; onChange: (v: boolean) => void; ariaLabel: string }) {
@@ -264,6 +265,67 @@ function StarterPluginCard({ onInstall, installing }: { onInstall: () => void; i
);
}
/* ─── Terminal Plugin Card ──────────────────────────────────────────────── */
function TerminalPluginCard({ onInstall, installing }: { onInstall: () => void; installing: boolean }) {
const { t } = useTranslation('settings');
return (
<div className="relative flex overflow-hidden rounded-lg border border-dashed border-border bg-card transition-all duration-200 hover:border-blue-400 dark:hover:border-blue-500">
<div className="w-[3px] flex-shrink-0 bg-blue-500/30" />
<div className="min-w-0 flex-1 p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 items-center gap-2.5">
<div className="h-5 w-5 flex-shrink-0 text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M7 8l4 4-4 4"/>
<line x1="13" y1="16" x2="17" y2="16"/>
</svg>
</div>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-semibold leading-none text-foreground">
{t('pluginSettings.terminalPlugin.name')}
</span>
<span className="rounded bg-blue-50 px-1.5 py-0.5 text-[10px] font-medium text-blue-600 dark:bg-blue-950/50 dark:text-blue-400">
{t('pluginSettings.terminalPlugin.badge')}
</span>
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
{t('pluginSettings.tab')}
</span>
</div>
<p className="mt-1 text-sm leading-snug text-muted-foreground">
{t('pluginSettings.terminalPlugin.description')}
</p>
<a
href={TERMINAL_PLUGIN_URL}
target="_blank"
rel="noopener noreferrer"
className="mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
>
<GitBranch className="h-3 w-3" />
cloudcli-ai/cloudcli-plugin-terminal
</a>
</div>
</div>
<button
onClick={onInstall}
disabled={installing}
className="flex flex-shrink-0 items-center gap-1.5 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
>
{installing ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Download className="h-3.5 w-3.5" />
)}
{installing ? t('pluginSettings.installing') : t('pluginSettings.terminalPlugin.install')}
</button>
</div>
</div>
</div>
);
}
/* ─── Main Component ────────────────────────────────────────────────────── */
export default function PluginSettingsTab() {
const { t } = useTranslation('settings');
@@ -273,6 +335,7 @@ export default function PluginSettingsTab() {
const [gitUrl, setGitUrl] = useState('');
const [installing, setInstalling] = useState(false);
const [installingStarter, setInstallingStarter] = useState(false);
const [installingTerminal, setInstallingTerminal] = useState(false);
const [installError, setInstallError] = useState<string | null>(null);
const [confirmUninstall, setConfirmUninstall] = useState<string | null>(null);
const [updatingPlugins, setUpdatingPlugins] = useState<Set<string>>(new Set());
@@ -311,6 +374,16 @@ export default function PluginSettingsTab() {
setInstallingStarter(false);
};
const handleInstallTerminal = async () => {
setInstallingTerminal(true);
setInstallError(null);
const result = await installPlugin(TERMINAL_PLUGIN_URL);
if (!result.success) {
setInstallError(result.error || t('pluginSettings.installFailed'));
}
setInstallingTerminal(false);
};
const handleUninstall = async (name: string) => {
if (confirmUninstall !== name) {
setConfirmUninstall(name);
@@ -326,6 +399,7 @@ export default function PluginSettingsTab() {
};
const hasStarterInstalled = plugins.some((p) => p.name === 'project-stats');
const hasTerminalInstalled = plugins.some((p) => p.name === 'web-terminal');
return (
<div className="space-y-6">
@@ -382,9 +456,16 @@ export default function PluginSettingsTab() {
</span>
</p>
{/* Starter plugin suggestion — above the list */}
{!loading && !hasStarterInstalled && (
<StarterPluginCard onInstall={handleInstallStarter} installing={installingStarter} />
{/* Official plugin suggestions — above the list */}
{!loading && (!hasStarterInstalled || !hasTerminalInstalled) && (
<div className="space-y-2">
{!hasStarterInstalled && (
<StarterPluginCard onInstall={handleInstallStarter} installing={installingStarter} />
)}
{!hasTerminalInstalled && (
<TerminalPluginCard onInstall={handleInstallTerminal} installing={installingTerminal} />
)}
</div>
)}
{/* Plugin List */}
@@ -423,33 +504,30 @@ export default function PluginSettingsTab() {
</div>
)}
{/* Build your own */}
<div className="flex items-center justify-between gap-4 border-t border-border/50 pt-2">
<div className="flex min-w-0 items-center gap-2">
<BookOpen className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/40" />
<span className="text-xs text-muted-foreground/60">
{t('pluginSettings.buildYourOwn')}
</span>
</div>
<div className="flex flex-shrink-0 items-center gap-3">
<a
href={STARTER_PLUGIN_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
>
{t('pluginSettings.starter')} <ExternalLink className="h-2.5 w-2.5" />
</a>
<span className="text-muted-foreground/20">·</span>
<a
href="https://cloudcli.ai/docs/plugin-overview"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
>
{t('pluginSettings.docs')} <ExternalLink className="h-2.5 w-2.5" />
</a>
</div>
{/* Starter plugin */}
<div className="flex items-center justify-center gap-3 border-t border-border/50 pt-2">
<BookOpen className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/40" />
<span className="text-xs text-muted-foreground/60">
{t('pluginSettings.starterPluginLabel')}
</span>
<span className="text-muted-foreground/20">·</span>
<a
href={STARTER_PLUGIN_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
>
{t('pluginSettings.starter')} <ExternalLink className="h-2.5 w-2.5" />
</a>
<span className="text-muted-foreground/20">·</span>
<a
href="https://cloudcli.ai/docs/plugin-overview"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
>
{t('pluginSettings.docs')} <ExternalLink className="h-2.5 w-2.5" />
</a>
</div>
</div>
);

View File

@@ -24,7 +24,7 @@ type ProviderLoginModalProps = {
const getProviderCommand = ({
provider,
customCommand,
isAuthenticated,
isAuthenticated: _isAuthenticated,
}: {
provider: CliProvider;
customCommand?: string;
@@ -35,10 +35,7 @@ const getProviderCommand = ({
}
if (provider === 'claude') {
if (isAuthenticated) {
return 'claude setup-token --dangerously-skip-permissions';
}
return 'claude /login --dangerously-skip-permissions';
return 'claude --dangerously-skip-permissions /login';
}
if (provider === 'cursor') {

View File

@@ -207,15 +207,23 @@ export default function Shell({
if (minimal) {
return (
<ShellMinimalView
terminalContainerRef={terminalContainerRef}
authUrl={authUrl}
authUrlVersion={authUrlVersion}
initialCommand={initialCommand}
isConnected={isConnected}
openAuthUrlInBrowser={openAuthUrlInBrowser}
copyAuthUrlToClipboard={copyAuthUrlToClipboard}
/>
<>
<ShellMinimalView
terminalContainerRef={terminalContainerRef}
authUrl={authUrl}
authUrlVersion={authUrlVersion}
initialCommand={initialCommand}
isConnected={isConnected}
openAuthUrlInBrowser={openAuthUrlInBrowser}
copyAuthUrlToClipboard={copyAuthUrlToClipboard}
/>
<TerminalShortcutsPanel
wsRef={wsRef}
terminalRef={terminalRef}
isConnected={isConnected}
bottomOffset="bottom-0"
/>
</>
);
}

View File

@@ -1,61 +1,65 @@
import { type MutableRefObject, useState, useCallback, useEffect, useRef } from 'react';
import { type MutableRefObject, useCallback, useState } from 'react';
import {
ChevronLeft,
ChevronRight,
Keyboard,
Clipboard,
ArrowDownToLine,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { Terminal } from '@xterm/xterm';
import { sendSocketMessage } from '../../utils/socket';
const SHORTCUTS = [
{ id: 'escape', labelKey: 'escape', sequence: '\x1b', hint: 'Esc' },
{ id: 'tab', labelKey: 'tab', sequence: '\t', hint: 'Tab' },
{ id: 'shift-tab', labelKey: 'shiftTab', sequence: '\x1b[Z', hint: '\u21e7Tab' },
{ id: 'arrow-up', labelKey: 'arrowUp', sequence: '\x1b[A', hint: '\u2191' },
{ id: 'arrow-down', labelKey: 'arrowDown', sequence: '\x1b[B', hint: '\u2193' },
] as const;
type Shortcut =
| { type: 'key'; id: string; label: string; sequence: string }
| { type: 'modifier'; id: string; label: string; modifier: 'ctrl' | 'alt' }
| { type: 'arrow'; id: string; sequence: string; icon: 'up' | 'down' | 'left' | 'right' };
const MOBILE_KEYS: Shortcut[] = [
{ type: 'key', id: 'esc', label: 'Esc', sequence: '\x1b' },
{ type: 'key', id: 'tab', label: 'Tab', sequence: '\t' },
{ type: 'key', id: 'shift-tab', label: '\u21e7Tab', sequence: '\x1b[Z' },
{ type: 'modifier', id: 'ctrl', label: 'CTRL', modifier: 'ctrl' },
{ type: 'modifier', id: 'alt', label: 'ALT', modifier: 'alt' },
{ type: 'arrow', id: 'arrow-up', sequence: '\x1b[A', icon: 'up' },
{ type: 'arrow', id: 'arrow-down', sequence: '\x1b[B', icon: 'down' },
{ type: 'arrow', id: 'arrow-left', sequence: '\x1b[D', icon: 'left' },
{ type: 'arrow', id: 'arrow-right', sequence: '\x1b[C', icon: 'right' },
];
const ARROW_ICONS = {
up: ArrowUp,
down: ArrowDown,
left: ArrowLeft,
right: ArrowRight,
} as const;
type TerminalShortcutsPanelProps = {
wsRef: MutableRefObject<WebSocket | null>;
terminalRef: MutableRefObject<Terminal | null>;
isConnected: boolean;
bottomOffset?: string;
};
const preventFocusSteal = (e: React.PointerEvent) => e.preventDefault();
const KEY_BTN =
'shrink-0 rounded-md border border-gray-600 bg-gray-700 px-2.5 py-1.5 text-xs font-medium text-gray-100 transition-colors select-none active:bg-blue-600 active:text-white active:border-blue-600 disabled:cursor-not-allowed disabled:opacity-40';
const KEY_BTN_ACTIVE =
'shrink-0 rounded-md border border-blue-500 bg-blue-600 px-2.5 py-1.5 text-xs font-medium text-white transition-colors select-none disabled:cursor-not-allowed disabled:opacity-40';
const ICON_BTN =
'shrink-0 rounded-md border border-gray-600 bg-gray-700 p-1.5 text-gray-100 transition-colors select-none active:bg-blue-600 active:text-white active:border-blue-600 disabled:cursor-not-allowed disabled:opacity-40';
export default function TerminalShortcutsPanel({
wsRef,
terminalRef,
isConnected,
bottomOffset = 'bottom-14',
}: TerminalShortcutsPanelProps) {
const { t } = useTranslation('settings');
const [isOpen, setIsOpen] = useState(false);
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => {
if (closeTimeoutRef.current) {
clearTimeout(closeTimeoutRef.current);
}
};
}, []);
const handleToggle = useCallback(() => {
setIsOpen((prev) => !prev);
}, []);
const handleShortcutAction = useCallback((action: () => void) => {
action();
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
if (closeTimeoutRef.current) {
clearTimeout(closeTimeoutRef.current);
}
closeTimeoutRef.current = setTimeout(() => setIsOpen(false), 50);
}, []);
const [ctrlActive, setCtrlActive] = useState(false);
const [altActive, setAltActive] = useState(false);
const sendInput = useCallback(
(data: string) => {
@@ -68,103 +72,120 @@ export default function TerminalShortcutsPanel({
terminalRef.current?.scrollToBottom();
}, [terminalRef]);
return (
<>
{/* Pull Tab */}
<button
type="button"
onPointerDown={preventFocusSteal}
onClick={handleToggle}
className={`fixed ${
isOpen ? 'right-64' : 'right-0'
} z-50 cursor-pointer rounded-l-md border border-gray-200 bg-white p-2 shadow-lg transition-all duration-150 ease-out hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700`}
style={{ top: '50%', transform: 'translateY(-50%)' }}
aria-label={
isOpen
? t('terminalShortcuts.handle.closePanel')
: t('terminalShortcuts.handle.openPanel')
const pasteFromClipboard = useCallback(async () => {
if (typeof navigator === 'undefined' || !navigator.clipboard?.readText) {
return;
}
try {
const text = await navigator.clipboard.readText();
if (text.length > 0) {
sendInput(text);
}
} catch {
// Ignore clipboard permission errors.
}
}, [sendInput]);
const handleKeyPress = useCallback(
(seq: string) => {
let finalSeq = seq;
if (ctrlActive && seq.length === 1) {
const code = seq.toLowerCase().charCodeAt(0);
if (code >= 97 && code <= 122) {
finalSeq = String.fromCharCode(code - 96);
}
>
{isOpen ? (
<ChevronRight className="h-5 w-5 text-gray-600 dark:text-gray-400" />
) : (
<ChevronLeft className="h-5 w-5 text-gray-600 dark:text-gray-400" />
)}
</button>
setCtrlActive(false);
}
if (altActive && seq.length === 1) {
finalSeq = '\x1b' + finalSeq;
setAltActive(false);
}
sendInput(finalSeq);
},
[ctrlActive, altActive, sendInput],
);
{/* Panel */}
<div
className={`fixed right-0 top-0 z-40 h-full w-64 transform border-l border-border bg-background shadow-xl transition-transform duration-150 ease-out ${
isOpen ? 'translate-x-0' : 'translate-x-full'
}`}
>
<div className="flex h-full flex-col">
{/* Header */}
<div className="border-b border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900">
<h3 className="flex items-center gap-2 text-lg font-semibold text-gray-900 dark:text-white">
<Keyboard className="h-5 w-5 text-gray-600 dark:text-gray-400" />
{t('terminalShortcuts.title')}
</h3>
</div>
{/* Content — conditionally rendered so buttons remount with clean CSS states */}
{isOpen && (
<div className="flex-1 space-y-6 overflow-y-auto overflow-x-hidden bg-background p-4">
{/* Shortcut Keys */}
<div className="space-y-2">
<h4 className="mb-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t('terminalShortcuts.sectionKeys')}
</h4>
{SHORTCUTS.map((shortcut) => (
<button
type="button"
key={shortcut.id}
onPointerDown={preventFocusSteal}
onClick={() => handleShortcutAction(() => sendInput(shortcut.sequence))}
disabled={!isConnected}
className="flex w-full items-center justify-between rounded-lg border border-transparent bg-gray-50 p-3 transition-colors hover:border-gray-300 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-gray-800 dark:hover:border-gray-600 dark:hover:bg-gray-700"
>
<span className="text-sm text-gray-900 dark:text-white">
{t(`terminalShortcuts.${shortcut.labelKey}`)}
</span>
<kbd className="rounded border border-gray-300 bg-gray-200 px-2 py-0.5 font-mono text-xs text-gray-600 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
{shortcut.hint}
</kbd>
</button>
))}
</div>
{/* Navigation */}
<div className="space-y-2">
<h4 className="mb-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t('terminalShortcuts.sectionNavigation')}
</h4>
<button
type="button"
onPointerDown={preventFocusSteal}
onClick={() => handleShortcutAction(scrollToBottom)}
disabled={!isConnected}
className="flex w-full items-center justify-between rounded-lg border border-transparent bg-gray-50 p-3 transition-colors hover:border-gray-300 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-gray-800 dark:hover:border-gray-600 dark:hover:bg-gray-700"
>
<span className="text-sm text-gray-900 dark:text-white">
{t('terminalShortcuts.scrollDown')}
</span>
<ArrowDownToLine className="h-4 w-4 text-gray-600 dark:text-gray-400" />
</button>
</div>
</div>
)}
</div>
</div>
{/* Backdrop */}
{isOpen && (
<div
className="fixed inset-0 z-30 bg-background/80 backdrop-blur-sm transition-opacity duration-150 ease-out"
return (
<div className={`pointer-events-none fixed inset-x-0 ${bottomOffset} z-20 px-2 md:hidden`}>
<div className="pointer-events-auto flex items-center gap-1 overflow-x-auto rounded-lg border border-gray-700/80 bg-gray-900/95 px-1.5 py-1.5 shadow-lg backdrop-blur-sm [-webkit-overflow-scrolling:touch] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<button
type="button"
onPointerDown={preventFocusSteal}
onClick={handleToggle}
/>
)}
</>
onClick={() => {
void pasteFromClipboard();
}}
disabled={!isConnected}
className={ICON_BTN}
title={t('terminalShortcuts.paste', { defaultValue: 'Paste' })}
aria-label={t('terminalShortcuts.paste', { defaultValue: 'Paste' })}
>
<Clipboard className="h-4 w-4" />
</button>
{MOBILE_KEYS.map((key) => {
if (key.type === 'modifier') {
const isActive = key.modifier === 'ctrl' ? ctrlActive : altActive;
const toggle =
key.modifier === 'ctrl'
? () => setCtrlActive((v) => !v)
: () => setAltActive((v) => !v);
return (
<button
type="button"
key={key.id}
onPointerDown={preventFocusSteal}
onClick={toggle}
disabled={!isConnected}
className={isActive ? KEY_BTN_ACTIVE : KEY_BTN}
>
{key.label}
</button>
);
}
if (key.type === 'arrow') {
const Icon = ARROW_ICONS[key.icon];
return (
<button
type="button"
key={key.id}
onPointerDown={preventFocusSteal}
onClick={() => sendInput(key.sequence)}
disabled={!isConnected}
className={ICON_BTN}
>
<Icon className="h-4 w-4" />
</button>
);
}
return (
<button
type="button"
key={key.id}
onPointerDown={preventFocusSteal}
onClick={() => handleKeyPress(key.sequence)}
disabled={!isConnected}
className={KEY_BTN}
>
{key.label}
</button>
);
})}
<button
type="button"
onPointerDown={preventFocusSteal}
onClick={scrollToBottom}
disabled={!isConnected}
className={ICON_BTN}
title={t('terminalShortcuts.scrollDown')}
aria-label={t('terminalShortcuts.scrollDown')}
>
<ArrowDownToLine className="h-4 w-4" />
</button>
</div>
</div>
);
}

View File

@@ -80,6 +80,29 @@ export default function SidebarProjectSessions({
return (
<div className="ml-3 space-y-1 border-l border-border pl-3">
<div className="px-3 pb-1 pt-1 md:hidden">
<button
className="flex h-8 w-full items-center justify-center gap-2 rounded-md bg-primary text-xs font-medium text-primary-foreground transition-all duration-150 hover:bg-primary/90 active:scale-[0.98]"
onClick={() => {
onProjectSelect(project);
onNewSession(project);
}}
>
<Plus className="h-3 w-3" />
{t('sessions.newSession')}
</button>
</div>
<Button
variant="default"
size="sm"
className="hidden h-8 w-full justify-start gap-2 bg-primary text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 md:flex"
onClick={() => onNewSession(project)}
>
<Plus className="h-3 w-3" />
{t('sessions.newSession')}
</Button>
{!initialSessionsLoaded ? (
<SessionListSkeleton />
) : !hasSessions && !isLoadingSessions ? (
@@ -129,29 +152,6 @@ export default function SidebarProjectSessions({
)}
</Button>
)}
<div className="px-3 pb-2 md:hidden">
<button
className="flex h-8 w-full items-center justify-center gap-2 rounded-md bg-primary text-xs font-medium text-primary-foreground transition-all duration-150 hover:bg-primary/90 active:scale-[0.98]"
onClick={() => {
onProjectSelect(project);
onNewSession(project);
}}
>
<Plus className="h-3 w-3" />
{t('sessions.newSession')}
</button>
</div>
<Button
variant="default"
size="sm"
className="mt-1 hidden h-8 w-full justify-start gap-2 bg-primary text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 md:flex"
onClick={() => onNewSession(project)}
>
<Plus className="h-3 w-3" />
{t('sessions.newSession')}
</Button>
</div>
);
}

View File

@@ -215,7 +215,7 @@
"viewFullRelease": "Vollständige Version anzeigen",
"updateProgress": "Update-Fortschritt:",
"manualUpgrade": "Manuelles Upgrade:",
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
"npmUpgradeCommand": "npm install -g @cloudcli-ai/cloudcli@latest",
"manualUpgradeHint": "Oder klick auf \"Jetzt aktualisieren\", um das Update automatisch durchzuführen.",
"updateCompleted": "Update erfolgreich abgeschlossen!",
"restartServer": "Bitte starte den Server neu, um die Änderungen anzuwenden.",

View File

@@ -245,7 +245,7 @@
"viewFullRelease": "View full release",
"updateProgress": "Update Progress:",
"manualUpgrade": "Manual upgrade:",
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
"npmUpgradeCommand": "npm install -g @cloudcli-ai/cloudcli@latest",
"manualUpgradeHint": "Or click \"Update Now\" to run the update automatically.",
"updateCompleted": "Update completed successfully!",
"restartServer": "Please restart the server to apply changes.",

View File

@@ -476,7 +476,7 @@
"installFailed": "Installation failed",
"uninstallFailed": "Uninstall failed",
"toggleFailed": "Toggle failed",
"buildYourOwn": "Build your own plugin",
"starterPluginLabel": "Starter Plugin",
"starter": "Starter",
"docs": "Docs",
"starterPlugin": {
@@ -485,6 +485,12 @@
"description": "File counts, lines of code, file-type breakdown, and recent activity for your project.",
"install": "Install"
},
"terminalPlugin": {
"name": "Terminal",
"badge": "official",
"description": "Integrated terminal with full shell access directly within the interface.",
"install": "Install"
},
"morePlugins": "More",
"enable": "Enable",
"disable": "Disable",

View File

@@ -245,7 +245,7 @@
"viewFullRelease": "リリース全文を見る",
"updateProgress": "アップデートの進捗:",
"manualUpgrade": "手動アップグレード:",
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
"npmUpgradeCommand": "npm install -g @cloudcli-ai/cloudcli@latest",
"manualUpgradeHint": "または「今すぐ更新」をクリックして自動的にアップデートを実行できます。",
"updateCompleted": "アップデートが完了しました!",
"restartServer": "変更を適用するにはサーバーを再起動してください。",

View File

@@ -245,7 +245,7 @@
"viewFullRelease": "전체 릴리스 보기",
"updateProgress": "업데이트 진행 상황:",
"manualUpgrade": "수동 업그레이드:",
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
"npmUpgradeCommand": "npm install -g @cloudcli-ai/cloudcli@latest",
"manualUpgradeHint": "또는 \"지금 업데이트\"를 클릭하여 자동으로 업데이트합니다.",
"updateCompleted": "업데이트가 완료되었습니다!",
"restartServer": "변경사항을 적용하려면 서버를 재시작하세요.",

View File

@@ -215,7 +215,7 @@
"viewFullRelease": "Посмотреть полный релиз",
"updateProgress": "Прогресс обновления:",
"manualUpgrade": "Ручное обновление:",
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
"npmUpgradeCommand": "npm install -g @cloudcli-ai/cloudcli@latest",
"manualUpgradeHint": "Или нажмите \"Обновить сейчас\" для автоматического обновления.",
"updateCompleted": "Обновление успешно завершено!",
"restartServer": "Пожалуйста, перезапустите сервер для применения изменений.",

View File

@@ -245,7 +245,7 @@
"viewFullRelease": "查看完整发布",
"updateProgress": "更新进度:",
"manualUpgrade": "手动升级:",
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
"npmUpgradeCommand": "npm install -g @cloudcli-ai/cloudcli@latest",
"manualUpgradeHint": "或点击'立即更新'以自动运行更新。",
"updateCompleted": "更新成功完成!",
"restartServer": "请重启服务器以应用更改。",