diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92635025..29cddae6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,14 +20,14 @@ jobs: contents: write id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.RELEASE_PAT }} - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 22 registry-url: https://registry.npmjs.org - name: git config diff --git a/CHANGELOG.md b/CHANGELOG.md index c884f3e8..149443d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,47 @@ All notable changes to CloudCLI UI will be documented in this file. +## [1.29.2](https://github.com/siteboon/claudecodeui/compare/v1.29.1...v1.29.2) (2026-04-14) + +### Bug Fixes + +* **sandbox:** use backgrounded sbx run to keep sandbox alive ([9b11c03](https://github.com/siteboon/claudecodeui/commit/9b11c034d9a19710a23b56c62dcf07c21a17bd97)) + +## [1.29.1](https://github.com/siteboon/claudecodeui/compare/v1.29.0...v1.29.1) (2026-04-14) + +### Bug Fixes + +* add latest tag to docker npx command and change the detach mode to work without spawn ([4a56972](https://github.com/siteboon/claudecodeui/commit/4a569725dae320a505753359d8edfd8ca79f0fd7)) + +## [1.29.0](https://github.com/siteboon/claudecodeui/compare/v1.28.1...v1.29.0) (2026-04-14) + +### New Features + +* adding docker sandbox environments ([13e97e2](https://github.com/siteboon/claudecodeui/commit/13e97e2c71254de7a60afb5495b21064c4bc4241)) + +### Bug Fixes + +* **thinking-mode:** fix dropdown positioning ([#646](https://github.com/siteboon/claudecodeui/issues/646)) ([c7a5baf](https://github.com/siteboon/claudecodeui/commit/c7a5baf1479404bd40e23aa58bd9f677df9a04c6)) + +### Maintenance + +* update release flow node version ([e2459cb](https://github.com/siteboon/claudecodeui/commit/e2459cb0f8b35f54827778a7b444e6c3ca326506)) + +## [1.28.1](https://github.com/siteboon/claudecodeui/compare/v1.28.0...v1.28.1) (2026-04-10) + +### New Features + +* add branding, community links, GitHub star badge, and About settings tab ([2207d05](https://github.com/siteboon/claudecodeui/commit/2207d05c1ca229214aa9c2e2c9f4d0827d421574)) + +### Bug Fixes + +* corrupted binary downloads ([#634](https://github.com/siteboon/claudecodeui/issues/634)) ([e61f8a5](https://github.com/siteboon/claudecodeui/commit/e61f8a543d63fe7c24a04b3d2186085a06dcbcdb)) +* **ui:** remove mobile bottom nav, unify processing indicator, and improve tooltip behavior on mobile ([#632](https://github.com/siteboon/claudecodeui/issues/632)) ([a8dab0e](https://github.com/siteboon/claudecodeui/commit/a8dab0edcf949ae610820bae9500c433781f7c73)) + +### Refactoring + +* remove unused whispher transcribe logic ([#637](https://github.com/siteboon/claudecodeui/issues/637)) ([590dd42](https://github.com/siteboon/claudecodeui/commit/590dd42649424ab990353fcf59ce0965036d3d25)) + ## [1.28.0](https://github.com/siteboon/claudecodeui/compare/v1.27.1...v1.28.0) (2026-04-03) ### New Features diff --git a/README.de.md b/README.de.md index 4e163781..461f59a8 100644 --- a/README.de.md +++ b/README.de.md @@ -76,6 +76,8 @@ Der schnellste Einstieg – keine lokale Einrichtung erforderlich. Erhalte eine ### Self-Hosted (Open Source) +#### npm + CloudCLI UI sofort mit **npx** ausprobieren (erfordert **Node.js** v22+): ```bash @@ -93,6 +95,15 @@ cloudcli Die **[Dokumentation →](https://cloudcli.ai/docs)** enthält weitere Konfigurationsoptionen, PM2, Remote-Server-Einrichtung und mehr. +#### Docker Sandboxes (Experimentell) + +Agents in isolierten Sandboxes mit Hypervisor-Isolation ausführen. Standardmäßig wird Claude Code gestartet. Erfordert die [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/). + +``` +npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project +``` + +Unterstützt Claude Code, Codex und Gemini CLI. Weitere Details in der [Sandbox-Dokumentation](docker/). --- diff --git a/README.ja.md b/README.ja.md index eada6dec..a3c7d04b 100644 --- a/README.ja.md +++ b/README.ja.md @@ -72,6 +72,8 @@ ### セルフホスト(オープンソース) +#### npm + **npx** で今すぐ CloudCLI UI を試せます(**Node.js** v22+ が必要): ```bash @@ -89,6 +91,15 @@ cloudcli より詳細な設定オプション、PM2、リモートサーバー設定などについては **[ドキュメントはこちら →](https://cloudcli.ai/docs)** を参照してください。 +#### Docker Sandboxes(実験的) + +ハイパーバイザーレベルの分離でエージェントをサンドボックスで実行します。デフォルトでは Claude Code が起動します。[`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/) が必要です。 + +``` +npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project +``` + +Claude Code、Codex、Gemini CLI に対応。詳細は[サンドボックスのドキュメント](docker/)をご覧ください。 --- diff --git a/README.ko.md b/README.ko.md index ed8ddd9f..f0c17471 100644 --- a/README.ko.md +++ b/README.ko.md @@ -72,6 +72,8 @@ ### 셀프 호스트 (오픈 소스) +#### npm + **npx**로 즉시 CloudCLI UI를 실행하세요 (Node.js v22+ 필요): ```bash @@ -87,7 +89,17 @@ cloudcli `http://localhost:3001`을 열면 기존 세션이 자동으로 발견됩니다. -자세한 구성 옵션, PM2, 원격 서버 설정 등은 **[문서 →](https://cloudcli.ai/docs)**를 참고하세요 +자세한 구성 옵션, PM2, 원격 서버 설정 등은 **[문서 →](https://cloudcli.ai/docs)**를 참고하세요. + +#### Docker Sandboxes (실험적) + +하이퍼바이저 수준 격리로 에이전트를 샌드박스에서 실행합니다. 기본 에이전트는 Claude Code입니다. [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/)가 필요합니다. + +``` +npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project +``` + +Claude Code, Codex, Gemini CLI를 지원합니다. 자세한 내용은 [샌드박스 문서](docker/)를 참고하세요. --- diff --git a/README.md b/README.md index 751f1659..b6f2cfb7 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ The fastest way to get started — no local setup required. Get a fully managed, ### Self-Hosted (Open source) +#### npm + Try CloudCLI UI instantly with **npx** (requires **Node.js** v22+): ``` @@ -91,33 +93,41 @@ 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 +Visit the **[documentation →](https://cloudcli.ai/docs)** for full configuration options, PM2, remote server setup and more. + +#### Docker Sandboxes (Experimental) + +Run agents in isolated sandboxes with hypervisor-level isolation. Starts Claude Code by default. Requires the [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/). + +``` +npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project +``` + +Supports Claude Code, Codex, and Gemini CLI. See the [sandbox docs](docker/) for setup and advanced options. --- ## 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 is the open source UI layer that powers CloudCLI Cloud. You can self-host it on your own machine, run it in a Docker sandbox for isolation, or use CloudCLI Cloud for a fully managed environment. -| | 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 | +| | Self-Hosted (npm) | Self-Hosted (Docker Sandbox) *(Experimental)* | CloudCLI Cloud | +|---|---|---|---| +| **Best for** | Local agent sessions on your own machine | Isolated agents with web/mobile IDE | Teams who want agents in the cloud | +| **How you access it** | Browser via `[yourip]:port` | Browser via `localhost:port` | Browser, any IDE, REST API, n8n | +| **Setup** | `npx @cloudcli-ai/cloudcli` | `npx @cloudcli-ai/cloudcli@latest sandbox ~/project` | No setup required | +| **Isolation** | Runs on your host | Hypervisor-level sandbox (microVM) | Full cloud isolation | +| **Machine needs to stay on** | Yes | Yes | No | +| **Mobile access** | Any browser on your network | Any browser on your network | Any device, native app coming | +| **Agents supported** | Claude Code, Cursor CLI, Codex, Gemini CLI | Claude Code, Codex, Gemini CLI | Claude Code, Cursor CLI, Codex, Gemini CLI | +| **File explorer and Git** | Yes | Yes | Yes | +| **MCP configuration** | Synced with `~/.claude` | Managed via UI | Managed via UI | +| **REST API** | Yes | Yes | Yes | +| **Team sharing** | No | No | Yes | +| **Platform cost** | Free, open source | Free, open source | Starts at $7/month | -> Both options use your own AI subscriptions (Claude, Cursor, etc.) — CloudCLI provides the environment, not the AI. +> All options use your own AI subscriptions (Claude, Cursor, etc.) — CloudCLI provides the environment, not the AI. --- diff --git a/README.ru.md b/README.ru.md index c7561ba6..a5907b38 100644 --- a/README.ru.md +++ b/README.ru.md @@ -76,6 +76,8 @@ ### Self-Hosted (Open source) +#### npm + Попробовать CloudCLI UI можно сразу через **npx** (требуется **Node.js** v22+): ```bash @@ -91,8 +93,17 @@ cloudcli Откройте `http://localhost:3001` — все ваши существующие сессии будут обнаружены автоматически. -Посетите **[документацию →](https://cloudcli.ai/docs)**, чтобы узнать про дополнительные варианты конфигурации, PM2, настройку удалённого сервера и многое другое +Посетите **[документацию →](https://cloudcli.ai/docs)**, чтобы узнать про дополнительные варианты конфигурации, PM2, настройку удалённого сервера и многое другое. +#### Docker Sandboxes (Экспериментально) + +Запускайте агентов в изолированных песочницах с гипервизорной изоляцией. По умолчанию запускается Claude Code. Требуется [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/). + +``` +npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project +``` + +Поддерживаются Claude Code, Codex и Gemini CLI. Подробнее в [документации sandbox](docker/). --- diff --git a/README.zh-CN.md b/README.zh-CN.md index 690af7d8..daf9a1a9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -72,6 +72,8 @@ ### 自托管(开源) +#### npm + 启动 CloudCLI UI,只需一行 `npx`(需要 Node.js v22+): ```bash @@ -87,7 +89,17 @@ cloudcli 打开 `http://localhost:3001`,系统会自动发现所有现有会话。 -更多配置选项、PM2、远程服务器设置等,请参阅 **[文档 →](https://cloudcli.ai/docs)** +更多配置选项、PM2、远程服务器设置等,请参阅 **[文档 →](https://cloudcli.ai/docs)**。 + +#### Docker Sandboxes(实验性) + +在隔离的沙箱中运行代理,具有虚拟机管理程序级别的隔离。默认启动 Claude Code。需要 [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/)。 + +``` +npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project +``` + +支持 Claude Code、Codex 和 Gemini CLI。详情请参阅 [沙箱文档](docker/)。 --- diff --git a/docker/README.md b/docker/README.md index 38cccb18..134ebad0 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,89 +1,160 @@ -# Claude Code UI — Docker Sandbox Templates + + -Run AI coding agents with a full web IDE inside [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/). +# Sandboxed coding agents with a web & mobile IDE (CloudCLI) -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. +[Docker Sandbox](https://docs.docker.com/ai/sandboxes/) templates that add [CloudCLI](https://cloudcli.ai) on top of Claude Code, Codex, and Gemini CLI. You get a full web and mobile IDE accessible from any browser on any device. -## Available Templates +## Get started -| 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 | +### 1. Install the sbx CLI -## Quick Start +Docker Sandboxes run agents in isolated microVMs. Install the `sbx` CLI: -### 1. Start a sandbox with the template +- **macOS**: `brew install docker/tap/sbx` +- **Windows**: `winget install -h Docker.sbx` +- **Linux**: `sudo apt-get install docker-sbx` + +Full instructions: [docs.docker.com/ai/sandboxes/get-started](https://docs.docker.com/ai/sandboxes/get-started/) + +### 2. Store your API key + +`sbx` manages credentials securely — your API key never enters the sandbox. Store it once: ```bash -sbx run --template docker.io/cloudcli-ai/sandbox:claude-code claude ~/my-project +sbx login +sbx secret set -g anthropic ``` -### 2. Forward the UI port +### 3. Launch Claude Code ```bash -sbx ports --publish 3001:3001 +npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project ``` -### 3. Open the browser +Open **http://localhost:3001**. Set a password on first visit. Start building. -``` -http://localhost:3001 +### Using a different agent + +Store the matching API key and pass `--agent`: + +```bash +# OpenAI Codex +sbx secret set -g openai +npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project --agent codex + +# Gemini CLI +sbx secret set -g google +npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project --agent gemini ``` -On first visit you'll set a password — this protects the UI if the port is ever exposed beyond localhost. +### Available templates -## What You Get +| Agent | Template | +|-------|----------| +| **Claude Code** (default) | `docker.io/cloudcliai/sandbox:claude-code` | +| OpenAI Codex | `docker.io/cloudcliai/sandbox:codex` | +| Gemini CLI | `docker.io/cloudcliai/sandbox:gemini` | -- **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 +These are used with `--template` when running `sbx` directly (see [Advanced usage](#advanced-usage)). + +## Managing sandboxes + +```bash +sbx ls # List all sandboxes +sbx stop my-project # Stop (preserves state) +sbx start my-project # Restart a stopped sandbox +sbx rm my-project # Remove everything +sbx exec my-project bash # Open a shell inside the sandbox +``` + +If you install CloudCLI globally (`npm install -g @cloudcli-ai/cloudcli`), you can also use: + +```bash +cloudcli sandbox ls +cloudcli sandbox start my-project # Restart and re-launch web UI +cloudcli sandbox logs my-project # View server logs +``` + +## What you get + +- **Chat** — Markdown rendering, code blocks, message history +- **Files** — File tree with syntax-highlighted editor +- **Git** — Diff viewer, staging, branch switching, commits - **Shell** — Built-in terminal emulator -- **MCP** — Configure Model Context Protocol servers through the UI +- **MCP** — Configure Model Context Protocol servers visually - **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. +Your project directory is mounted bidirectionally — edits propagate in real time, both ways. ## 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: +Set variables at creation time with `--env`: ```bash -sbx policy allow network "localhost:3001" +npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project --env SERVER_PORT=8080 ``` +Or inside a running sandbox: + +```bash +sbx exec my-project bash -c 'echo "export SERVER_PORT=8080" >> /etc/sandbox-persistent.sh' +``` + +Restart CloudCLI for changes to take effect: + +```bash +sbx exec my-project bash -c 'pkill -f "server/index.js"' +sbx exec -d my-project cloudcli start --port 3001 +``` + +| Variable | Default | Description | +|----------|---------|-------------| +| `SERVER_PORT` | `3001` | Web UI port | +| `HOST` | `0.0.0.0` | Bind address (must be `0.0.0.0` for `sbx ports`) | +| `DATABASE_PATH` | `~/.cloudcli/auth.db` | SQLite database location | + +## Advanced usage + +For branch mode, multiple workspaces, memory limits, or the terminal agent experience, use `sbx` with the template: + +```bash +# Terminal agent + web UI +sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/my-project --name my-project +sbx ports my-project --publish 3001:3001 + +# Branch mode (Git worktree isolation) +sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/my-project --branch my-feature + +# Multiple workspaces +sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/project ~/shared-libs:ro + +# Pass a prompt directly +sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/my-project -- "Fix the auth bug" +``` + +CloudCLI auto-starts via `.bashrc` when using `sbx run`. + +Full options in the [Docker Sandboxes usage guide](https://docs.docker.com/ai/sandboxes/usage/). + +## Network policies + +Sandboxes restrict outbound access by default. To reach host services from inside the sandbox: + +```bash +sbx policy allow network localhost:11434 +# Inside the sandbox: curl http://host.docker.internal:11434 +``` + +The web UI itself doesn't need a policy — access it via `sbx ports`. + +## Links + +- [CloudCLI Cloud](https://cloudcli.ai) — fully managed, no setup required +- [Documentation](https://cloudcli.ai/docs) — full configuration guide +- [Discord](https://discord.gg/buxwujPNRE) — community support +- [GitHub](https://github.com/siteboon/claudecodeui) — source code and issues + ## License -These templates are free and open-source under the same license as Claude Code UI (AGPL-3.0-or-later). +AGPL-3.0-or-later diff --git a/docker/claude-code/Dockerfile b/docker/claude-code/Dockerfile index ed04cf6b..8d108a80 100644 --- a/docker/claude-code/Dockerfile +++ b/docker/claude-code/Dockerfile @@ -5,7 +5,7 @@ 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 +RUN npm install -g @cloudcli-ai/cloudcli && cloudcli --version -COPY shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh +COPY --chown=agent:agent shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc diff --git a/docker/codex/Dockerfile b/docker/codex/Dockerfile index 9948d0af..c1bc1807 100644 --- a/docker/codex/Dockerfile +++ b/docker/codex/Dockerfile @@ -5,7 +5,7 @@ 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 +RUN npm install -g @cloudcli-ai/cloudcli && cloudcli --version -COPY shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh +COPY --chown=agent:agent shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc diff --git a/docker/gemini/Dockerfile b/docker/gemini/Dockerfile index ec7209f0..dc06db69 100644 --- a/docker/gemini/Dockerfile +++ b/docker/gemini/Dockerfile @@ -5,7 +5,7 @@ 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 +RUN npm install -g @cloudcli-ai/cloudcli && cloudcli --version -COPY shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh +COPY --chown=agent:agent shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc diff --git a/docker/shared/install-cloudcli.sh b/docker/shared/install-cloudcli.sh index 0390383e..4b43fe22 100644 --- a/docker/shared/install-cloudcli.sh +++ b/docker/shared/install-cloudcli.sh @@ -1,13 +1,10 @@ #!/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 \ +# Install build tools needed for native modules (node-pty, better-sqlite3, bcrypt) +# Node.js is already provided by the sandbox base image +apt-get update && apt-get install -y --no-install-recommends \ + build-essential python3 python3-setuptools \ jq ripgrep sqlite3 zip unzip tree vim-tiny # Clean up apt cache to reduce image size diff --git a/docker/shared/start-cloudcli.sh b/docker/shared/start-cloudcli.sh index 26b6c8c0..82b2fdc2 100644 --- a/docker/shared/start-cloudcli.sh +++ b/docker/shared/start-cloudcli.sh @@ -1,22 +1,17 @@ #!/bin/bash -# Auto-start Claude Code UI server in background if not already running. +# Auto-start CloudCLI 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 " CloudCLI is starting on port 3001..." echo "" - echo " To access the web UI, forward the port:" - echo " sbx ports \$(hostname) --publish 3001:3001" + echo " Forward the port from another terminal:" + echo " sbx ports --publish 3001:3001" echo "" echo " Then open: http://localhost:3001" echo "" diff --git a/package-lock.json b/package-lock.json index c1c675b0..21bee5b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cloudcli-ai/cloudcli", - "version": "1.28.0", + "version": "1.29.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cloudcli-ai/cloudcli", - "version": "1.28.0", + "version": "1.29.2", "hasInstallScript": true, "license": "AGPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 7a446d88..2a80d5a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cloudcli-ai/cloudcli", - "version": "1.28.0", + "version": "1.29.2", "description": "A web-based UI for Claude Code CLI", "type": "module", "main": "server/index.js", @@ -35,7 +35,8 @@ "release": "./release.sh", "prepublishOnly": "npm run build", "postinstall": "node scripts/fix-node-pty.js", - "prepare": "husky" + "prepare": "husky", + "update:platform": "./update-platform.sh" }, "keywords": [ "claude code", diff --git a/public/api-docs.html b/public/api-docs.html index ec671ecc..1d86cf4c 100644 --- a/public/api-docs.html +++ b/public/api-docs.html @@ -3,7 +3,7 @@ - Claude Code UI - API Documentation + CloudCLI - API Documentation @@ -418,7 +418,7 @@
-

Claude Code UI

+

CloudCLI

API Documentation
diff --git a/public/sw.js b/public/sw.js index 9b4351bc..b909bf55 100755 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -// Service Worker for Claude Code UI PWA +// Service Worker for CloudCLI PWA // 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'; @@ -79,7 +79,7 @@ self.addEventListener('push', event => { try { payload = event.data.json(); } catch { - payload = { title: 'Claude Code UI', body: event.data.text() }; + payload = { title: 'CloudCLI', body: event.data.text() }; } const options = { @@ -92,7 +92,7 @@ self.addEventListener('push', event => { }; event.waitUntil( - self.registration.showNotification(payload.title || 'Claude Code UI', options) + self.registration.showNotification(payload.title || 'CloudCLI', options) ); }); diff --git a/server/cli.js b/server/cli.js index 8359b6d7..3c0d1d4f 100755 --- a/server/cli.js +++ b/server/cli.js @@ -1,12 +1,13 @@ #!/usr/bin/env node /** - * Claude Code UI CLI + * CloudCLI CLI * - * Provides command-line utilities for managing Claude Code UI + * Provides command-line utilities for managing CloudCLI * * Commands: * (no args) - Start the server (default) * start - Start the server + * sandbox - Manage Docker sandbox environments * status - Show configuration and data locations * help - Show help information * version - Show version information @@ -84,7 +85,7 @@ function getInstallDir() { // Show status command function showStatus() { - console.log(`\n${c.bright('Claude Code UI - Status')}\n`); + console.log(`\n${c.bright('CloudCLI UI - Status')}\n`); console.log(c.dim('═'.repeat(60))); // Version info @@ -141,7 +142,7 @@ function showStatus() { function showHelp() { console.log(` ╔═══════════════════════════════════════════════════════════════╗ -║ Claude Code UI - Command Line Tool ║ +║ CloudCLI - Command Line Tool ║ ╚═══════════════════════════════════════════════════════════════╝ Usage: @@ -149,7 +150,8 @@ Usage: cloudcli [command] [options] Commands: - start Start the Claude Code UI server (default) + start Start the CloudCLI server (default) + sandbox Manage Docker sandbox environments status Show configuration and data locations update Update to the latest version help Show this help information @@ -164,8 +166,7 @@ Options: Examples: $ cloudcli # Start with defaults $ cloudcli --port 8080 # Start on port 8080 - $ cloudcli -p 3000 # Short form for port - $ cloudcli start --port 4000 # Explicit start command + $ cloudcli sandbox ~/my-project # Run in a Docker sandbox $ cloudcli status # Show configuration Environment Variables: @@ -244,6 +245,353 @@ async function updatePackage() { } } +// ── Sandbox command ───────────────────────────────────────── + +const SANDBOX_TEMPLATES = { + claude: 'docker.io/cloudcliai/sandbox:claude-code', + codex: 'docker.io/cloudcliai/sandbox:codex', + gemini: 'docker.io/cloudcliai/sandbox:gemini', +}; + +const SANDBOX_SECRETS = { + claude: 'anthropic', + codex: 'openai', + gemini: 'google', +}; + +function parseSandboxArgs(args) { + const result = { + subcommand: null, + workspace: null, + agent: 'claude', + name: null, + port: 3001, + template: null, + env: [], + }; + + const subcommands = ['ls', 'stop', 'start', 'rm', 'logs', 'help']; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (i === 0 && subcommands.includes(arg)) { + result.subcommand = arg; + } else if (arg === '--agent' || arg === '-a') { + result.agent = args[++i]; + } else if (arg === '--name' || arg === '-n') { + result.name = args[++i]; + } else if (arg === '--port') { + result.port = parseInt(args[++i], 10); + } else if (arg === '--template' || arg === '-t') { + result.template = args[++i]; + } else if (arg === '--env' || arg === '-e') { + result.env.push(args[++i]); + } else if (!arg.startsWith('-')) { + if (!result.subcommand) { + result.workspace = arg; + } else { + result.name = arg; // for stop/start/rm/logs + } + } + } + + // Default subcommand based on what we got + if (!result.subcommand) { + result.subcommand = 'create'; + } + + // Derive name from workspace path if not set + if (!result.name && result.workspace) { + result.name = path.basename(path.resolve(result.workspace.replace(/^~/, os.homedir()))); + } + + // Default template from agent + if (!result.template) { + result.template = SANDBOX_TEMPLATES[result.agent] || SANDBOX_TEMPLATES.claude; + } + + return result; +} + +function showSandboxHelp() { + console.log(` +${c.bright('CloudCLI Sandbox')} — Run CloudCLI inside Docker Sandboxes + +Usage: + cloudcli sandbox Create and start a sandbox + cloudcli sandbox [name] Manage sandboxes + +Subcommands: + ${c.bright('(default)')} Create a sandbox and start the web UI + ${c.bright('ls')} List all sandboxes + ${c.bright('start')} Restart a stopped sandbox and re-launch the web UI + ${c.bright('stop')} Stop a sandbox (preserves state) + ${c.bright('rm')} Remove a sandbox + ${c.bright('logs')} Show CloudCLI server logs + ${c.bright('help')} Show this help + +Options: + -a, --agent Agent to use: claude, codex, gemini (default: claude) + -n, --name Sandbox name (default: derived from workspace folder) + -t, --template Custom template image + -e, --env Set environment variable (repeatable) + --port Host port for the web UI (default: 3001) + +Examples: + $ cloudcli sandbox ~/my-project + $ cloudcli sandbox ~/my-project --agent codex --port 8080 + $ cloudcli sandbox ~/my-project --env SERVER_PORT=8080 --env HOST=0.0.0.0 + $ cloudcli sandbox ls + $ cloudcli sandbox stop my-project + $ cloudcli sandbox start my-project + $ cloudcli sandbox rm my-project + +Prerequisites: + 1. Install sbx CLI: https://docs.docker.com/ai/sandboxes/get-started/ + 2. Authenticate and store your API key: + sbx login + sbx secret set -g anthropic # for Claude + sbx secret set -g openai # for Codex + sbx secret set -g google # for Gemini + +Advanced usage: + For branch mode, multiple workspaces, memory limits, network policies, + or passing prompts to the agent, use sbx directly with the template: + + sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/my-project --branch my-feature + sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/project ~/libs:ro --memory 8g + + Full Docker Sandboxes docs: https://docs.docker.com/ai/sandboxes/usage/ +`); +} + +async function sandboxCommand(args) { + const { execFileSync, spawn: spawnProcess } = await import('child_process'); + + // Safe execution — uses execFileSync (no shell) to prevent injection + const sbx = (subcmd, opts = {}) => { + const result = execFileSync('sbx', subcmd, { + encoding: 'utf8', + stdio: opts.inherit ? 'inherit' : 'pipe', + }); + return result || ''; + }; + + const opts = parseSandboxArgs(args); + + if (opts.subcommand === 'help') { + showSandboxHelp(); + return; + } + + // Validate name (alphanumeric, hyphens, underscores only) + if (opts.name && !/^[\w-]+$/.test(opts.name)) { + console.error(`\n${c.error('❌')} Invalid sandbox name: ${opts.name}`); + console.log(` Names may only contain letters, numbers, hyphens, and underscores.\n`); + process.exit(1); + } + + // Check sbx is installed + try { + sbx(['version']); + } catch { + console.error(`\n${c.error('❌')} ${c.bright('sbx')} CLI not found.\n`); + console.log(` Install it from: ${c.info('https://docs.docker.com/ai/sandboxes/get-started/')}`); + console.log(` Then run: ${c.bright('sbx login')}`); + console.log(` And store your API key: ${c.bright('sbx secret set -g anthropic')}\n`); + process.exit(1); + } + + switch (opts.subcommand) { + + case 'ls': + sbx(['ls'], { inherit: true }); + break; + + case 'stop': + if (!opts.name) { + console.error(`\n${c.error('❌')} Sandbox name required: cloudcli sandbox stop \n`); + process.exit(1); + } + sbx(['stop', opts.name], { inherit: true }); + break; + + case 'rm': + if (!opts.name) { + console.error(`\n${c.error('❌')} Sandbox name required: cloudcli sandbox rm \n`); + process.exit(1); + } + sbx(['rm', opts.name], { inherit: true }); + break; + + case 'logs': + if (!opts.name) { + console.error(`\n${c.error('❌')} Sandbox name required: cloudcli sandbox logs \n`); + process.exit(1); + } + try { + sbx(['exec', opts.name, 'bash', '-c', 'cat /tmp/cloudcli-ui.log'], { inherit: true }); + } catch (e) { + console.error(`\n${c.error('❌')} Could not read logs: ${e.message || 'Is the sandbox running?'}\n`); + } + break; + + case 'start': { + if (!opts.name) { + console.error(`\n${c.error('❌')} Sandbox name required: cloudcli sandbox start \n`); + process.exit(1); + } + console.log(`\n${c.info('▶')} Starting sandbox ${c.bright(opts.name)}...`); + const restartRun = spawnProcess('sbx', ['run', opts.name], { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'], + }); + restartRun.unref(); + await new Promise(resolve => setTimeout(resolve, 5000)); + + console.log(`${c.info('▶')} Launching CloudCLI web server...`); + sbx(['exec', opts.name, 'bash', '-c', 'cloudcli start --port 3001 &']); + + console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`); + try { + sbx(['ports', opts.name, '--publish', `${opts.port}:3001`]); + } catch (e) { + const msg = e.stdout || e.stderr || e.message || ''; + if (msg.includes('address already in use')) { + const altPort = opts.port + 1; + console.log(`${c.warn('⚠')} Port ${opts.port} in use, trying ${altPort}...`); + try { + sbx(['ports', opts.name, '--publish', `${altPort}:3001`]); + opts.port = altPort; + } catch { + console.error(`${c.error('❌')} Ports ${opts.port} and ${altPort} both in use. Use --port to specify a free port.`); + process.exit(1); + } + } else { + throw e; + } + } + + console.log(`\n${c.ok('✔')} ${c.bright('CloudCLI is ready!')}`); + console.log(` ${c.info('→')} ${c.bright(`http://localhost:${opts.port}`)}\n`); + break; + } + + case 'create': { + if (!opts.workspace) { + console.error(`\n${c.error('❌')} Workspace path required: cloudcli sandbox \n`); + console.log(` Example: ${c.bright('cloudcli sandbox ~/my-project')}\n`); + process.exit(1); + } + + const workspace = opts.workspace.startsWith('~') + ? opts.workspace.replace(/^~/, os.homedir()) + : path.resolve(opts.workspace); + + if (!fs.existsSync(workspace)) { + console.error(`\n${c.error('❌')} Workspace path not found: ${c.dim(workspace)}\n`); + process.exit(1); + } + + const secret = SANDBOX_SECRETS[opts.agent] || 'anthropic'; + + // Check if the required secret is stored + try { + const secretList = sbx(['secret', 'ls']); + if (!secretList.includes(secret)) { + console.error(`\n${c.error('❌')} No ${c.bright(secret)} API key found.\n`); + console.log(` Run: ${c.bright(`sbx secret set -g ${secret}`)}\n`); + process.exit(1); + } + } catch { /* sbx secret ls not available, skip check */ } + + console.log(`\n${c.bright('CloudCLI Sandbox')}`); + console.log(c.dim('─'.repeat(50))); + console.log(` Agent: ${c.info(opts.agent)} ${c.dim(`(${secret} credentials)`)}`); + console.log(` Workspace: ${c.dim(workspace)}`); + console.log(` Name: ${c.dim(opts.name)}`); + console.log(` Template: ${c.dim(opts.template)}`); + console.log(` Port: ${c.dim(String(opts.port))}`); + if (opts.env.length > 0) { + console.log(` Env: ${c.dim(opts.env.join(', '))}`); + } + console.log(c.dim('─'.repeat(50))); + + // Step 1: Launch sandbox with sbx run in background. + // sbx run creates the sandbox (or reconnects) AND holds an active session, + // which prevents the sandbox from auto-stopping. + console.log(`\n${c.info('▶')} Creating sandbox ${c.bright(opts.name)}...`); + const bgRun = spawnProcess('sbx', [ + 'run', '--template', opts.template, '--name', opts.name, opts.agent, workspace, + ], { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'], + }); + bgRun.unref(); + // Wait for sandbox to be ready + await new Promise(resolve => setTimeout(resolve, 5000)); + + // Step 2: Inject environment variables + if (opts.env.length > 0) { + console.log(`${c.info('▶')} Setting environment variables...`); + const exports = opts.env + .filter(e => /^\w+=.+$/.test(e)) + .map(e => `export ${e}`) + .join('\n'); + if (exports) { + sbx(['exec', opts.name, 'bash', '-c', `echo '${exports}' >> /etc/sandbox-persistent.sh`]); + } + const invalid = opts.env.filter(e => !/^\w+=.+$/.test(e)); + if (invalid.length > 0) { + console.log(`${c.warn('⚠')} Skipped invalid env vars: ${invalid.join(', ')} (expected KEY=VALUE)`); + } + } + + // Step 3: Start CloudCLI inside the sandbox + console.log(`${c.info('▶')} Launching CloudCLI web server...`); + sbx(['exec', opts.name, 'bash', '-c', 'cloudcli start --port 3001 &']); + + // Step 4: Forward port + console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`); + try { + sbx(['ports', opts.name, '--publish', `${opts.port}:3001`]); + } catch (e) { + const msg = e.stdout || e.stderr || e.message || ''; + if (msg.includes('address already in use')) { + const altPort = opts.port + 1; + console.log(`${c.warn('⚠')} Port ${opts.port} in use, trying ${altPort}...`); + try { + sbx(['ports', opts.name, '--publish', `${altPort}:3001`]); + opts.port = altPort; + } catch { + console.error(`${c.error('❌')} Ports ${opts.port} and ${altPort} both in use. Use --port to specify a free port.`); + process.exit(1); + } + } else { + throw e; + } + } + + // Done + console.log(`\n${c.ok('✔')} ${c.bright('CloudCLI is ready!')}`); + console.log(` ${c.info('→')} Open ${c.bright(`http://localhost:${opts.port}`)}`); + console.log(`\n${c.dim(' Manage with:')}`); + console.log(` ${c.dim('$')} sbx ls`); + console.log(` ${c.dim('$')} sbx stop ${opts.name}`); + console.log(` ${c.dim('$')} sbx start ${opts.name}`); + console.log(` ${c.dim('$')} sbx rm ${opts.name}`); + console.log(`\n${c.dim(' Or install globally:')} npm install -g @cloudcli-ai/cloudcli\n`); + break; + } + + default: + showSandboxHelp(); + } +} + +// ── Server ────────────────────────────────────────────────── + // Start the server async function startServer() { // Check for updates silently on startup @@ -274,6 +622,10 @@ function parseArgs(args) { parsed.command = 'version'; } else if (!arg.startsWith('-')) { parsed.command = arg; + if (arg === 'sandbox') { + parsed.remainingArgs = args.slice(i + 1); + break; + } } } @@ -283,7 +635,7 @@ function parseArgs(args) { // Main CLI handler async function main() { const args = process.argv.slice(2); - const { command, options } = parseArgs(args); + const { command, options, remainingArgs } = parseArgs(args); // Apply CLI options to environment variables if (options.serverPort) { @@ -299,6 +651,9 @@ async function main() { case 'start': await startServer(); break; + case 'sandbox': + await sandboxCommand(remainingArgs || []); + break; case 'status': case 'info': showStatus(); diff --git a/server/index.js b/server/index.js index 5e7fd605..ccffb661 100755 --- a/server/index.js +++ b/server/index.js @@ -435,13 +435,20 @@ app.post('/api/system/update', authenticateToken, async (req, res) => { console.log('Starting system update from directory:', projectRoot); - // Run the update command based on install mode - const updateCommand = installMode === 'git' - ? 'git checkout main && git pull && npm install' - : 'npm install -g @cloudcli-ai/cloudcli@latest'; + // Platform deployments use their own update workflow from the project root. + const updateCommand = IS_PLATFORM + // In platform, husky and dev dependencies are not needed + ? 'npm run update:platform' + : installMode === 'git' + ? 'git checkout main && git pull && npm install' + : 'npm install -g @cloudcli-ai/cloudcli@latest'; + + const updateCwd = IS_PLATFORM || installMode === 'git' + ? projectRoot + : os.homedir(); const child = spawn('sh', ['-c', updateCommand], { - cwd: installMode === 'git' ? projectRoot : os.homedir(), + cwd: updateCwd, env: process.env }); @@ -1984,155 +1991,6 @@ function handleShellConnection(ws) { console.error('[ERROR] Shell WebSocket error:', error); }); } -// Audio transcription endpoint -app.post('/api/transcribe', authenticateToken, async (req, res) => { - try { - const multer = (await import('multer')).default; - const upload = multer({ storage: multer.memoryStorage() }); - - // Handle multipart form data - upload.single('audio')(req, res, async (err) => { - if (err) { - return res.status(400).json({ error: 'Failed to process audio file' }); - } - - if (!req.file) { - return res.status(400).json({ error: 'No audio file provided' }); - } - - const apiKey = process.env.OPENAI_API_KEY; - if (!apiKey) { - return res.status(500).json({ error: 'OpenAI API key not configured. Please set OPENAI_API_KEY in server environment.' }); - } - - try { - // Create form data for OpenAI - const FormData = (await import('form-data')).default; - const formData = new FormData(); - formData.append('file', req.file.buffer, { - filename: req.file.originalname, - contentType: req.file.mimetype - }); - formData.append('model', 'whisper-1'); - formData.append('response_format', 'json'); - formData.append('language', 'en'); - - // Make request to OpenAI - const response = await fetch('https://api.openai.com/v1/audio/transcriptions', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${apiKey}`, - ...formData.getHeaders() - }, - body: formData - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error?.message || `Whisper API error: ${response.status}`); - } - - const data = await response.json(); - let transcribedText = data.text || ''; - - // Check if enhancement mode is enabled - const mode = req.body.mode || 'default'; - - // If no transcribed text, return empty - if (!transcribedText) { - return res.json({ text: '' }); - } - - // If default mode, return transcribed text without enhancement - if (mode === 'default') { - return res.json({ text: transcribedText }); - } - - // Handle different enhancement modes - try { - const OpenAI = (await import('openai')).default; - const openai = new OpenAI({ apiKey }); - - let prompt, systemMessage, temperature = 0.7, maxTokens = 800; - - switch (mode) { - case 'prompt': - systemMessage = 'You are an expert prompt engineer who creates clear, detailed, and effective prompts.'; - prompt = `You are an expert prompt engineer. Transform the following rough instruction into a clear, detailed, and context-aware AI prompt. - -Your enhanced prompt should: -1. Be specific and unambiguous -2. Include relevant context and constraints -3. Specify the desired output format -4. Use clear, actionable language -5. Include examples where helpful -6. Consider edge cases and potential ambiguities - -Transform this rough instruction into a well-crafted prompt: -"${transcribedText}" - -Enhanced prompt:`; - break; - - case 'vibe': - case 'instructions': - case 'architect': - systemMessage = 'You are a helpful assistant that formats ideas into clear, actionable instructions for AI agents.'; - temperature = 0.5; // Lower temperature for more controlled output - prompt = `Transform the following idea into clear, well-structured instructions that an AI agent can easily understand and execute. - -IMPORTANT RULES: -- Format as clear, step-by-step instructions -- Add reasonable implementation details based on common patterns -- Only include details directly related to what was asked -- Do NOT add features or functionality not mentioned -- Keep the original intent and scope intact -- Use clear, actionable language an agent can follow - -Transform this idea into agent-friendly instructions: -"${transcribedText}" - -Agent instructions:`; - break; - - default: - // No enhancement needed - break; - } - - // Only make GPT call if we have a prompt - if (prompt) { - const completion = await openai.chat.completions.create({ - model: 'gpt-4o-mini', - messages: [ - { role: 'system', content: systemMessage }, - { role: 'user', content: prompt } - ], - temperature: temperature, - max_tokens: maxTokens - }); - - transcribedText = completion.choices[0].message.content || transcribedText; - } - - } catch (gptError) { - console.error('GPT processing error:', gptError); - // Fall back to original transcription if GPT fails - } - - res.json({ text: transcribedText }); - - } catch (error) { - console.error('Transcription error:', error); - res.status(500).json({ error: error.message }); - } - }); - } catch (error) { - console.error('Endpoint error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - // Image upload endpoint app.post('/api/projects/:projectName/upload-images', authenticateToken, async (req, res) => { try { @@ -2548,7 +2406,7 @@ async function startServer() { console.log(''); console.log(c.dim('═'.repeat(63))); - console.log(` ${c.bright('Claude Code UI Server - Ready')}`); + console.log(` ${c.bright('CloudCLI Server - Ready')}`); console.log(c.dim('═'.repeat(63))); console.log(''); console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + SERVER_PORT)}`); diff --git a/server/routes/agent.js b/server/routes/agent.js index 6c97e2fb..06ecb86b 100644 --- a/server/routes/agent.js +++ b/server/routes/agent.js @@ -1125,7 +1125,7 @@ router.post('/', validateExternalApiKey, async (req, res) => { } else { prBody += `Agent task: ${message}`; } - prBody += '\n\n---\n*This pull request was automatically created by Claude Code UI Agent.*'; + prBody += '\n\n---\n*This pull request was automatically created by CloudCLI.ai Agent.*'; console.log(`📝 PR Title: ${prTitle}`); diff --git a/server/services/notification-orchestrator.js b/server/services/notification-orchestrator.js index bb573e10..d3d47dd6 100644 --- a/server/services/notification-orchestrator.js +++ b/server/services/notification-orchestrator.js @@ -125,7 +125,7 @@ function buildPushBody(event) { const message = CODE_MAP[event.code] || 'You have a new notification'; return { - title: sessionName || 'Claude Code UI', + title: sessionName || 'CloudCLI', body: `${providerLabel}: ${message}`, data: { sessionId: event.sessionId || null, diff --git a/shared/modelConstants.js b/shared/modelConstants.js index 514a1772..389fb633 100644 --- a/shared/modelConstants.js +++ b/shared/modelConstants.js @@ -18,9 +18,10 @@ export const CLAUDE_MODELS = { { value: "haiku", label: "Haiku" }, { value: "opusplan", label: "Opus Plan" }, { value: "sonnet[1m]", label: "Sonnet [1M]" }, + { value: "opus[1m]", label: "Opus [1M]" }, ], - DEFAULT: "sonnet", + DEFAULT: "opus", }; /** @@ -58,6 +59,7 @@ export const CURSOR_MODELS = { export const CODEX_MODELS = { OPTIONS: [ { value: "gpt-5.4", label: "GPT-5.4" }, + { value: "gpt-5.4-mini", label: "GPT-5.4 mini" }, { value: "gpt-5.3-codex", label: "GPT-5.3 Codex" }, { value: "gpt-5.2-codex", label: "GPT-5.2 Codex" }, { value: "gpt-5.2", label: "GPT-5.2" }, @@ -88,5 +90,5 @@ export const GEMINI_MODELS = { }, ], - DEFAULT: "gemini-2.5-flash", + DEFAULT: "gemini-3.1-pro-preview", }; diff --git a/src/components/auth/view/AuthLoadingScreen.tsx b/src/components/auth/view/AuthLoadingScreen.tsx index 36ecbc2f..9b4987d5 100644 --- a/src/components/auth/view/AuthLoadingScreen.tsx +++ b/src/components/auth/view/AuthLoadingScreen.tsx @@ -12,7 +12,7 @@ export default function AuthLoadingScreen() { -

Claude Code UI

+

CloudCLI

{loadingDotAnimationDelays.map((delay) => ( diff --git a/src/components/auth/view/AuthScreenLayout.tsx b/src/components/auth/view/AuthScreenLayout.tsx index 6651dbe0..d53ff95f 100644 --- a/src/components/auth/view/AuthScreenLayout.tsx +++ b/src/components/auth/view/AuthScreenLayout.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react'; import { MessageSquare } from 'lucide-react'; +import { IS_PLATFORM } from '../../../constants/config'; type AuthScreenLayoutProps = { title: string; @@ -37,6 +38,22 @@ export default function AuthScreenLayout({

{footerText}

+ + {!IS_PLATFORM && ( + + )}
diff --git a/src/components/auth/view/LoginForm.tsx b/src/components/auth/view/LoginForm.tsx index 4e0973e2..cf26ca3d 100644 --- a/src/components/auth/view/LoginForm.tsx +++ b/src/components/auth/view/LoginForm.tsx @@ -58,7 +58,7 @@ export default function LoginForm() {
} diff --git a/src/components/chat/hooks/useChatComposerState.ts b/src/components/chat/hooks/useChatComposerState.ts index 6e84982d..858faff9 100644 --- a/src/components/chat/hooks/useChatComposerState.ts +++ b/src/components/chat/hooks/useChatComposerState.ts @@ -878,30 +878,6 @@ export function useChatComposerState({ }); }, [canAbortSession, currentSessionId, pendingViewSessionRef, provider, selectedSession?.id, sendMessage]); - const handleTranscript = useCallback((text: string) => { - if (!text.trim()) { - return; - } - - setInput((previousInput) => { - const newInput = previousInput.trim() ? `${previousInput} ${text}` : text; - inputValueRef.current = newInput; - - setTimeout(() => { - if (!textareaRef.current) { - return; - } - - textareaRef.current.style.height = 'auto'; - textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; - const lineHeight = parseInt(window.getComputedStyle(textareaRef.current).lineHeight); - setIsTextareaExpanded(textareaRef.current.scrollHeight > lineHeight * 2); - }, 0); - - return newInput; - }); - }, []); - const handleGrantToolPermission = useCallback( (suggestion: { entry: string; toolName: string }) => { if (!suggestion || provider !== 'claude') { @@ -994,7 +970,6 @@ export function useChatComposerState({ syncInputOverlayScroll, handleClearInput, handleAbortSession, - handleTranscript, handlePermissionDecision, handleGrantToolPermission, handleInputFocusChange, diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx index cb78222c..19483f64 100644 --- a/src/components/chat/view/ChatInterface.tsx +++ b/src/components/chat/view/ChatInterface.tsx @@ -165,7 +165,6 @@ function ChatInterface({ syncInputOverlayScroll, handleClearInput, handleAbortSession, - handleTranscript, handlePermissionDecision, handleGrantToolPermission, handleInputFocusChange, @@ -407,7 +406,6 @@ function ChatInterface({ })} isTextareaExpanded={isTextareaExpanded} sendByCtrlEnter={sendByCtrlEnter} - onTranscript={handleTranscript} /> diff --git a/src/components/chat/view/subcomponents/ChatComposer.tsx b/src/components/chat/view/subcomponents/ChatComposer.tsx index 2bf8eb50..e6da236d 100644 --- a/src/components/chat/view/subcomponents/ChatComposer.tsx +++ b/src/components/chat/view/subcomponents/ChatComposer.tsx @@ -11,7 +11,6 @@ import type { SetStateAction, TouchEvent, } from 'react'; -import MicButton from '../../../mic-button/view/MicButton'; import type { PendingPermissionRequest, PermissionMode, Provider } from '../../types/types'; import CommandMenu from './CommandMenu'; import ClaudeStatus from './ClaudeStatus'; @@ -91,7 +90,6 @@ interface ChatComposerProps { placeholder: string; isTextareaExpanded: boolean; sendByCtrlEnter?: boolean; - onTranscript: (text: string) => void; } export default function ChatComposer({ @@ -148,7 +146,6 @@ export default function ChatComposer({ placeholder, isTextareaExpanded, sendByCtrlEnter, - onTranscript, }: ChatComposerProps) { const { t } = useTranslation('chat'); const textareaRect = textareaRef.current?.getBoundingClientRect(); @@ -321,10 +318,6 @@ export default function ChatComposer({ -
- -
- - {isOpen && ( -
+ {isOpen && typeof document !== 'undefined' && createPortal( +

{t('thinkingMode.selector.title')}

-
+
{translatedModes.map((mode) => { const ModeIcon = mode.icon; const isSelected = mode.id === selectedMode; @@ -91,10 +190,10 @@ function ThinkingModeSelector({ selectedMode, onModeChange, onClose, className = return (
-
+
, + document.body )}
); } -export default ThinkingModeSelector; \ No newline at end of file +export default ThinkingModeSelector; diff --git a/src/components/git-panel/view/changes/CommitComposer.tsx b/src/components/git-panel/view/changes/CommitComposer.tsx index 521ea068..da1113b2 100644 --- a/src/components/git-panel/view/changes/CommitComposer.tsx +++ b/src/components/git-panel/view/changes/CommitComposer.tsx @@ -1,6 +1,5 @@ import { Check, ChevronDown, GitCommit, RefreshCw, Sparkles } from 'lucide-react'; import { useState } from 'react'; -import MicButton from '../../../mic-button/view/MicButton'; import type { ConfirmationRequest } from '../../types/types'; // Persists commit messages across unmount/remount, keyed by project path @@ -147,13 +146,6 @@ export default function CommitComposer({ )} -
- setCommitMessage(transcript)} - mode="default" - className="p-1.5" - /> -
diff --git a/src/components/mic-button/constants/constants.ts b/src/components/mic-button/constants/constants.ts deleted file mode 100644 index 3bfbe62d..00000000 --- a/src/components/mic-button/constants/constants.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { MicButtonState } from '../types/types'; - -export const MIC_BUTTON_STATES = { - IDLE: 'idle', - RECORDING: 'recording', - TRANSCRIBING: 'transcribing', - PROCESSING: 'processing', -} as const; - -export const MIC_TAP_DEBOUNCE_MS = 300; -export const PROCESSING_STATE_DELAY_MS = 2000; - -export const DEFAULT_WHISPER_MODE = 'default'; - -// Modes that use post-transcription enhancement on the backend. -export const ENHANCEMENT_WHISPER_MODES = new Set([ - 'prompt', - 'vibe', - 'instructions', - 'architect', -]); - -export const BUTTON_BACKGROUND_BY_STATE: Record = { - idle: '#374151', - recording: '#ef4444', - transcribing: '#3b82f6', - processing: '#a855f7', -}; - -export const MIC_ERROR_BY_NAME = { - NotAllowedError: 'Microphone access denied. Please allow microphone permissions.', - NotFoundError: 'No microphone found. Please check your audio devices.', - NotSupportedError: 'Microphone not supported by this browser.', - NotReadableError: 'Microphone is being used by another application.', -} as const; - -export const MIC_NOT_AVAILABLE_ERROR = - 'Microphone access not available. Please use HTTPS or a supported browser.'; - -export const MIC_NOT_SUPPORTED_ERROR = - 'Microphone not supported. Please use HTTPS or a modern browser.'; - -export const MIC_SECURE_CONTEXT_ERROR = - 'Microphone requires HTTPS. Please use a secure connection.'; - diff --git a/src/components/mic-button/data/whisper.ts b/src/components/mic-button/data/whisper.ts deleted file mode 100644 index be204d32..00000000 --- a/src/components/mic-button/data/whisper.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { api } from '../../../utils/api'; - -type WhisperStatus = 'transcribing'; - -type WhisperResponse = { - text?: string; - error?: string; -}; - -export async function transcribeWithWhisper( - audioBlob: Blob, - onStatusChange?: (status: WhisperStatus) => void, -): Promise { - const formData = new FormData(); - const fileName = `recording_${Date.now()}.webm`; - const file = new File([audioBlob], fileName, { type: audioBlob.type }); - - formData.append('audio', file); - - const whisperMode = window.localStorage.getItem('whisperMode') || 'default'; - formData.append('mode', whisperMode); - - try { - // Keep existing status callback behavior. - if (onStatusChange) { - onStatusChange('transcribing'); - } - - const response = (await api.transcribe(formData)) as Response; - - if (!response.ok) { - const errorData = (await response.json().catch(() => ({}))) as WhisperResponse; - throw new Error( - errorData.error || - `Transcription error: ${response.status} ${response.statusText}`, - ); - } - - const data = (await response.json()) as WhisperResponse; - return data.text || ''; - } catch (error) { - if ( - error instanceof Error - && error.name === 'TypeError' - && error.message.includes('fetch') - ) { - throw new Error('Cannot connect to server. Please ensure the backend is running.'); - } - throw error; - } -} - diff --git a/src/components/mic-button/hooks/useMicButtonController.ts b/src/components/mic-button/hooks/useMicButtonController.ts deleted file mode 100644 index dfddec78..00000000 --- a/src/components/mic-button/hooks/useMicButtonController.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import type { MouseEvent } from 'react'; -import { transcribeWithWhisper } from '../data/whisper'; -import { - DEFAULT_WHISPER_MODE, - ENHANCEMENT_WHISPER_MODES, - MIC_BUTTON_STATES, - MIC_ERROR_BY_NAME, - MIC_NOT_AVAILABLE_ERROR, - MIC_NOT_SUPPORTED_ERROR, - MIC_SECURE_CONTEXT_ERROR, - MIC_TAP_DEBOUNCE_MS, - PROCESSING_STATE_DELAY_MS, -} from '../constants/constants'; -import type { MicButtonState } from '../types/types'; - -type UseMicButtonControllerArgs = { - onTranscript?: (transcript: string) => void; -}; - -type UseMicButtonControllerResult = { - state: MicButtonState; - error: string | null; - isSupported: boolean; - handleButtonClick: (event?: MouseEvent) => void; -}; - -const getRecordingErrorMessage = (error: unknown): string => { - if (error instanceof Error && error.message.includes('HTTPS')) { - return error.message; - } - - if (error instanceof DOMException) { - return MIC_ERROR_BY_NAME[error.name as keyof typeof MIC_ERROR_BY_NAME] || 'Microphone access failed'; - } - - return 'Microphone access failed'; -}; - -const getRecorderMimeType = (): string => ( - MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4' -); - -export function useMicButtonController({ - onTranscript, -}: UseMicButtonControllerArgs): UseMicButtonControllerResult { - const [state, setState] = useState(MIC_BUTTON_STATES.IDLE); - const [error, setError] = useState(null); - const [isSupported, setIsSupported] = useState(true); - - const mediaRecorderRef = useRef(null); - const streamRef = useRef(null); - const chunksRef = useRef([]); - const lastTapRef = useRef(0); - const processingTimerRef = useRef(null); - - const clearProcessingTimer = (): void => { - if (processingTimerRef.current !== null) { - window.clearTimeout(processingTimerRef.current); - processingTimerRef.current = null; - } - }; - - const stopStreamTracks = (): void => { - if (!streamRef.current) { - return; - } - - streamRef.current.getTracks().forEach((track) => track.stop()); - streamRef.current = null; - }; - - const handleStopRecording = async (mimeType: string): Promise => { - const audioBlob = new Blob(chunksRef.current, { type: mimeType }); - - // Release the microphone immediately once recording ends. - stopStreamTracks(); - setState(MIC_BUTTON_STATES.TRANSCRIBING); - - const whisperMode = window.localStorage.getItem('whisperMode') || DEFAULT_WHISPER_MODE; - const shouldShowProcessingState = ENHANCEMENT_WHISPER_MODES.has(whisperMode); - - if (shouldShowProcessingState) { - processingTimerRef.current = window.setTimeout(() => { - setState(MIC_BUTTON_STATES.PROCESSING); - }, PROCESSING_STATE_DELAY_MS); - } - - try { - const transcript = await transcribeWithWhisper(audioBlob); - if (transcript && onTranscript) { - onTranscript(transcript); - } - } catch (transcriptionError) { - const message = transcriptionError instanceof Error ? transcriptionError.message : 'Transcription error'; - setError(message); - } finally { - clearProcessingTimer(); - setState(MIC_BUTTON_STATES.IDLE); - } - }; - - const startRecording = async (): Promise => { - try { - setError(null); - chunksRef.current = []; - - if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { - throw new Error(MIC_NOT_AVAILABLE_ERROR); - } - - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - streamRef.current = stream; - - const mimeType = getRecorderMimeType(); - const recorder = new MediaRecorder(stream, { mimeType }); - mediaRecorderRef.current = recorder; - - recorder.ondataavailable = (event: BlobEvent) => { - if (event.data.size > 0) { - chunksRef.current.push(event.data); - } - }; - - recorder.onstop = () => { - void handleStopRecording(mimeType); - }; - - recorder.start(); - setState(MIC_BUTTON_STATES.RECORDING); - } catch (recordingError) { - stopStreamTracks(); - setError(getRecordingErrorMessage(recordingError)); - setState(MIC_BUTTON_STATES.IDLE); - } - }; - - const stopRecording = (): void => { - if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') { - mediaRecorderRef.current.stop(); - return; - } - - stopStreamTracks(); - setState(MIC_BUTTON_STATES.IDLE); - }; - - const handleButtonClick = (event?: MouseEvent): void => { - if (event) { - event.preventDefault(); - event.stopPropagation(); - } - - if (!isSupported) { - return; - } - - // Mobile tap handling can trigger duplicate click events in quick succession. - const now = Date.now(); - if (now - lastTapRef.current < MIC_TAP_DEBOUNCE_MS) { - return; - } - lastTapRef.current = now; - - if (state === MIC_BUTTON_STATES.IDLE) { - void startRecording(); - return; - } - - if (state === MIC_BUTTON_STATES.RECORDING) { - stopRecording(); - } - }; - - useEffect(() => { - // getUserMedia needs both browser support and a secure context. - if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { - setIsSupported(false); - setError(MIC_NOT_SUPPORTED_ERROR); - return; - } - - if (location.protocol !== 'https:' && location.hostname !== 'localhost') { - setIsSupported(false); - setError(MIC_SECURE_CONTEXT_ERROR); - return; - } - - setIsSupported(true); - setError(null); - }, []); - - useEffect(() => () => { - clearProcessingTimer(); - stopStreamTracks(); - }, []); - - return { - state, - error, - isSupported, - handleButtonClick, - }; -} diff --git a/src/components/mic-button/types/types.ts b/src/components/mic-button/types/types.ts deleted file mode 100644 index c0469822..00000000 --- a/src/components/mic-button/types/types.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type MicButtonState = 'idle' | 'recording' | 'transcribing' | 'processing'; - diff --git a/src/components/mic-button/view/MicButton.tsx b/src/components/mic-button/view/MicButton.tsx deleted file mode 100644 index 72926cef..00000000 --- a/src/components/mic-button/view/MicButton.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useMicButtonController } from '../hooks/useMicButtonController'; -import MicButtonView from './MicButtonView'; - -type MicButtonProps = { - onTranscript?: (transcript: string) => void; - className?: string; - mode?: string; -}; - -export default function MicButton({ - onTranscript, - className = '', - mode: _mode, -}: MicButtonProps) { - const { state, error, isSupported, handleButtonClick } = useMicButtonController({ - onTranscript, - }); - - // Keep `mode` in the public props for backwards compatibility. - void _mode; - - return ( - - ); -} - diff --git a/src/components/mic-button/view/MicButtonView.tsx b/src/components/mic-button/view/MicButtonView.tsx deleted file mode 100644 index 7471ca5c..00000000 --- a/src/components/mic-button/view/MicButtonView.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Brain, Loader2, Mic } from 'lucide-react'; -import type { MouseEvent, ReactElement } from 'react'; -import { BUTTON_BACKGROUND_BY_STATE, MIC_BUTTON_STATES } from '../constants/constants'; -import type { MicButtonState } from '../types/types'; - -type MicButtonViewProps = { - state: MicButtonState; - error: string | null; - isSupported: boolean; - className: string; - onButtonClick: (event?: MouseEvent) => void; -}; - -const getButtonIcon = (state: MicButtonState, isSupported: boolean): ReactElement => { - if (!isSupported) { - return ; - } - - if (state === MIC_BUTTON_STATES.TRANSCRIBING) { - return ; - } - - if (state === MIC_BUTTON_STATES.PROCESSING) { - return ; - } - - if (state === MIC_BUTTON_STATES.RECORDING) { - return ; - } - - return ; -}; - -export default function MicButtonView({ - state, - error, - isSupported, - className, - onButtonClick, -}: MicButtonViewProps) { - const isDisabled = !isSupported || state === MIC_BUTTON_STATES.TRANSCRIBING || state === MIC_BUTTON_STATES.PROCESSING; - const icon = getButtonIcon(state, isSupported); - - return ( -
- - - {error && ( -
- {error} -
- )} - - {state === MIC_BUTTON_STATES.RECORDING && ( -
- )} - - {state === MIC_BUTTON_STATES.PROCESSING && ( -
- )} -
- ); -} diff --git a/src/components/quick-settings-panel/constants.ts b/src/components/quick-settings-panel/constants.ts index 5f1a8e21..15c15458 100644 --- a/src/components/quick-settings-panel/constants.ts +++ b/src/components/quick-settings-panel/constants.ts @@ -2,21 +2,12 @@ import { ArrowDown, Brain, Eye, - FileText, Languages, Maximize2, - Mic, - Sparkles, } from 'lucide-react'; -import type { - PreferenceToggleItem, - WhisperMode, - WhisperOption, -} from './types'; +import type { PreferenceToggleItem } from './types'; export const HANDLE_POSITION_STORAGE_KEY = 'quickSettingsHandlePosition'; -export const WHISPER_MODE_STORAGE_KEY = 'whisperMode'; -export const WHISPER_MODE_CHANGED_EVENT = 'whisperModeChanged'; export const DEFAULT_HANDLE_POSITION = 50; export const HANDLE_POSITION_MIN = 10; @@ -64,30 +55,3 @@ export const INPUT_SETTING_TOGGLES: PreferenceToggleItem[] = [ icon: Languages, }, ]; - -export const WHISPER_OPTIONS: WhisperOption[] = [ - { - value: 'default', - titleKey: 'quickSettings.whisper.modes.default', - descriptionKey: 'quickSettings.whisper.modes.defaultDescription', - icon: Mic, - }, - { - value: 'prompt', - titleKey: 'quickSettings.whisper.modes.prompt', - descriptionKey: 'quickSettings.whisper.modes.promptDescription', - icon: Sparkles, - }, - { - value: 'vibe', - titleKey: 'quickSettings.whisper.modes.vibe', - descriptionKey: 'quickSettings.whisper.modes.vibeDescription', - icon: FileText, - }, -]; - -export const VIBE_MODE_ALIASES: WhisperMode[] = [ - 'vibe', - 'instructions', - 'architect', -]; diff --git a/src/components/quick-settings-panel/hooks/useWhisperMode.ts b/src/components/quick-settings-panel/hooks/useWhisperMode.ts deleted file mode 100644 index eeda67ce..00000000 --- a/src/components/quick-settings-panel/hooks/useWhisperMode.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { useCallback, useState } from 'react'; -import { - VIBE_MODE_ALIASES, - WHISPER_MODE_CHANGED_EVENT, - WHISPER_MODE_STORAGE_KEY, -} from '../constants'; -import type { WhisperMode, WhisperOptionValue } from '../types'; - -const ALL_VALID_MODES: WhisperMode[] = [ - 'default', - 'prompt', - 'vibe', - 'instructions', - 'architect', -]; - -const isWhisperMode = (value: string): value is WhisperMode => ( - ALL_VALID_MODES.includes(value as WhisperMode) -); - -const readStoredMode = (): WhisperMode => { - if (typeof window === 'undefined') { - return 'default'; - } - - const storedValue = localStorage.getItem(WHISPER_MODE_STORAGE_KEY); - if (!storedValue) { - return 'default'; - } - - return isWhisperMode(storedValue) ? storedValue : 'default'; -}; - -export function useWhisperMode() { - const [whisperMode, setWhisperModeState] = useState(readStoredMode); - - const setWhisperMode = useCallback((value: WhisperOptionValue) => { - setWhisperModeState(value); - localStorage.setItem(WHISPER_MODE_STORAGE_KEY, value); - window.dispatchEvent(new Event(WHISPER_MODE_CHANGED_EVENT)); - }, []); - - const isOptionSelected = useCallback( - (value: WhisperOptionValue) => { - if (value === 'vibe') { - return VIBE_MODE_ALIASES.includes(whisperMode); - } - - return whisperMode === value; - }, - [whisperMode], - ); - - return { - whisperMode, - setWhisperMode, - isOptionSelected, - }; -} diff --git a/src/components/quick-settings-panel/types.ts b/src/components/quick-settings-panel/types.ts index 4a12fc01..16002694 100644 --- a/src/components/quick-settings-panel/types.ts +++ b/src/components/quick-settings-panel/types.ts @@ -16,20 +16,4 @@ export type PreferenceToggleItem = { icon: LucideIcon; }; -export type WhisperMode = - | 'default' - | 'prompt' - | 'vibe' - | 'instructions' - | 'architect'; - -export type WhisperOptionValue = 'default' | 'prompt' | 'vibe'; - -export type WhisperOption = { - value: WhisperOptionValue; - titleKey: string; - descriptionKey: string; - icon: LucideIcon; -}; - export type QuickSettingsHandleStyle = CSSProperties; diff --git a/src/components/quick-settings-panel/view/QuickSettingsContent.tsx b/src/components/quick-settings-panel/view/QuickSettingsContent.tsx index 60d19912..8d805fe9 100644 --- a/src/components/quick-settings-panel/view/QuickSettingsContent.tsx +++ b/src/components/quick-settings-panel/view/QuickSettingsContent.tsx @@ -15,7 +15,6 @@ import type { } from '../types'; import QuickSettingsSection from './QuickSettingsSection'; import QuickSettingsToggleRow from './QuickSettingsToggleRow'; -import QuickSettingsWhisperSection from './QuickSettingsWhisperSection'; type QuickSettingsContentProps = { isDarkMode: boolean; @@ -73,8 +72,6 @@ export default function QuickSettingsContent({ {t('quickSettings.sendByCtrlEnterDescription')}

- -
); } diff --git a/src/components/quick-settings-panel/view/QuickSettingsWhisperSection.tsx b/src/components/quick-settings-panel/view/QuickSettingsWhisperSection.tsx deleted file mode 100644 index 9eb5f744..00000000 --- a/src/components/quick-settings-panel/view/QuickSettingsWhisperSection.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { TOGGLE_ROW_CLASS, WHISPER_OPTIONS } from '../constants'; -import { useWhisperMode } from '../hooks/useWhisperMode'; -import QuickSettingsSection from './QuickSettingsSection'; - -export default function QuickSettingsWhisperSection() { - const { t } = useTranslation('settings'); - const { setWhisperMode, isOptionSelected } = useWhisperMode(); - - return ( - // This section stays hidden intentionally until dictation modes are reintroduced. - -
- {WHISPER_OPTIONS.map(({ value, icon: Icon, titleKey, descriptionKey }) => ( - - ))} -
-
- ); -} diff --git a/src/components/settings/types/types.ts b/src/components/settings/types/types.ts index 096059ce..e3af730b 100644 --- a/src/components/settings/types/types.ts +++ b/src/components/settings/types/types.ts @@ -1,6 +1,6 @@ import type { Dispatch, SetStateAction } from 'react'; -export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins'; +export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins' | 'about'; export type AgentProvider = 'claude' | 'cursor' | 'codex' | 'gemini'; export type AgentCategory = 'account' | 'permissions' | 'mcp'; export type ProjectSortOrder = 'name' | 'date'; diff --git a/src/components/settings/view/PremiumFeatureCard.tsx b/src/components/settings/view/PremiumFeatureCard.tsx new file mode 100644 index 00000000..07980d86 --- /dev/null +++ b/src/components/settings/view/PremiumFeatureCard.tsx @@ -0,0 +1,46 @@ +import { ExternalLink, Lock } from 'lucide-react'; +import type { ReactNode } from 'react'; + +const CLOUDCLI_URL = 'https://cloudcli.ai'; + +type PremiumFeatureCardProps = { + icon: ReactNode; + title: string; + description: string; + ctaText?: string; +}; + +export default function PremiumFeatureCard({ + icon, + title, + description, + ctaText = 'Available with CloudCLI Pro', +}: PremiumFeatureCardProps) { + return ( +
+
+
+ {icon} +
+
+
+

{title}

+ +
+

+ {description} +

+ + {ctaText} + + +
+
+
+ ); +} diff --git a/src/components/settings/view/Settings.tsx b/src/components/settings/view/Settings.tsx index 444d0e06..70d3ea4d 100644 --- a/src/components/settings/view/Settings.tsx +++ b/src/components/settings/view/Settings.tsx @@ -12,6 +12,7 @@ import GitSettingsTab from '../view/tabs/git-settings/GitSettingsTab'; import NotificationsSettingsTab from '../view/tabs/NotificationsSettingsTab'; import TasksSettingsTab from '../view/tabs/tasks-settings/TasksSettingsTab'; import PluginSettingsTab from '../../plugins/view/PluginSettingsTab'; +import AboutTab from '../view/tabs/AboutTab'; import { useSettingsController } from '../hooks/useSettingsController'; import { useWebPush } from '../../../hooks/useWebPush'; import type { SettingsProps } from '../types/types'; @@ -206,6 +207,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set {activeTab === 'api' && } {activeTab === 'plugins' && } + + {activeTab === 'about' && }
diff --git a/src/components/settings/view/SettingsMainTabs.tsx b/src/components/settings/view/SettingsMainTabs.tsx index d085d545..5c15f6de 100644 --- a/src/components/settings/view/SettingsMainTabs.tsx +++ b/src/components/settings/view/SettingsMainTabs.tsx @@ -1,4 +1,4 @@ -import { GitBranch, Key, Puzzle } from 'lucide-react'; +import { GitBranch, Info, Key, Puzzle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { SettingsMainTab } from '../types/types'; @@ -22,6 +22,7 @@ const TAB_CONFIG: MainTabConfig[] = [ { id: 'tasks', labelKey: 'mainTabs.tasks' }, { id: 'notifications', labelKey: 'mainTabs.notifications' }, { id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle }, + { id: 'about', labelKey: 'mainTabs.about', icon: Info }, ]; export default function SettingsMainTabs({ activeTab, onChange }: SettingsMainTabsProps) { diff --git a/src/components/settings/view/SettingsSidebar.tsx b/src/components/settings/view/SettingsSidebar.tsx index e6f56f46..149c1492 100644 --- a/src/components/settings/view/SettingsSidebar.tsx +++ b/src/components/settings/view/SettingsSidebar.tsx @@ -1,4 +1,4 @@ -import { Bell, Bot, GitBranch, Key, ListChecks, Palette, Puzzle } from 'lucide-react'; +import { Bell, Bot, GitBranch, Info, Key, ListChecks, Palette, Puzzle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { cn } from '../../../lib/utils'; import { PillBar, Pill } from '../../../shared/view/ui'; @@ -23,6 +23,7 @@ const NAV_ITEMS: NavItem[] = [ { id: 'tasks', labelKey: 'mainTabs.tasks', icon: ListChecks }, { id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle }, { id: 'notifications', labelKey: 'mainTabs.notifications', icon: Bell }, + { id: 'about', labelKey: 'mainTabs.about', icon: Info }, ]; export default function SettingsSidebar({ activeTab, onChange }: SettingsSidebarProps) { diff --git a/src/components/settings/view/tabs/AboutTab.tsx b/src/components/settings/view/tabs/AboutTab.tsx new file mode 100644 index 00000000..027af75e --- /dev/null +++ b/src/components/settings/view/tabs/AboutTab.tsx @@ -0,0 +1,166 @@ +import { ExternalLink, MessageSquare, Star } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { IS_PLATFORM } from '../../../../constants/config'; +import { useVersionCheck } from '../../../../hooks/useVersionCheck'; +import PremiumFeatureCard from '../PremiumFeatureCard'; +import { Cloud, Users } from 'lucide-react'; + +const GITHUB_REPO_URL = 'https://github.com/siteboon/claudecodeui'; +const DISCORD_URL = 'https://discord.gg/buxwujPNRE'; +const DOCS_URL = 'https://cloudcli.ai/docs/plugin-overview'; +const CLOUDCLI_URL = 'https://cloudcli.ai'; + +function GitHubIcon({ className }: { className?: string }) { + return ( + + ); +} + +function DiscordIcon({ className }: { className?: string }) { + return ( + + ); +} + +export default function AboutTab() { + const { t } = useTranslation('settings'); + const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui'); + const releasesUrl = releaseInfo?.htmlUrl || `${GITHUB_REPO_URL}/releases`; + + return ( +
+ {/* Logo + name + version */} +
+
+ +
+
+ +

+ Open-source AI coding assistant interface +

+
+
+ + {/* Star on GitHub button */} + + + + Star on GitHub + + + {/* Links */} + + + {/* Hosted CTA (OSS mode only) */} + {!IS_PLATFORM && ( +
+

Try CloudCLI Hosted

+

+ Team collaboration, shared MCP configs, settings sync across environments, and managed infrastructure. +

+ + Learn more + + +
+ )} + + {/* Premium feature placeholders (OSS mode only) */} + {!IS_PLATFORM && ( +
+

CloudCLI Pro Features

+ } + title="Sync Settings" + description="Keep your preferences, MCP configs, and theme in sync across all your environments." + /> + } + title="Team Management" + description="Multiple users, role-based access, and shared projects for your team." + /> +
+ )} + + {/* License */} +
+

+ Licensed under AGPL-3.0 +

+
+
+ ); +} diff --git a/src/components/settings/view/tabs/agents-settings/sections/content/McpServersContent.tsx b/src/components/settings/view/tabs/agents-settings/sections/content/McpServersContent.tsx index 9db223a1..c62f8f8f 100644 --- a/src/components/settings/view/tabs/agents-settings/sections/content/McpServersContent.tsx +++ b/src/components/settings/view/tabs/agents-settings/sections/content/McpServersContent.tsx @@ -1,6 +1,8 @@ -import { Edit3, Globe, Plus, Server, Terminal, Trash2, Zap } from 'lucide-react'; +import { Edit3, Globe, Plus, Server, Terminal, Trash2, Users, Zap } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Badge, Button } from '../../../../../../../shared/view/ui'; +import { IS_PLATFORM } from '../../../../../../../constants/config'; +import PremiumFeatureCard from '../../../../PremiumFeatureCard'; import type { McpServer, McpToolsResult, McpTestResult } from '../../../../../types/types'; const getTransportIcon = (type: string | undefined) => { @@ -179,6 +181,14 @@ function ClaudeMcpServers({
{t('mcpServers.empty')}
)} + + {!IS_PLATFORM && ( + } + title="Team MCP Configs" + description="Share MCP server configurations across your team. Everyone stays in sync automatically." + /> + )} ); } diff --git a/src/components/settings/view/tabs/api-settings/CredentialsSettingsTab.tsx b/src/components/settings/view/tabs/api-settings/CredentialsSettingsTab.tsx index 0f475125..89d440c3 100644 --- a/src/components/settings/view/tabs/api-settings/CredentialsSettingsTab.tsx +++ b/src/components/settings/view/tabs/api-settings/CredentialsSettingsTab.tsx @@ -1,14 +1,11 @@ import { useTranslation } from 'react-i18next'; -import { useVersionCheck } from '../../../../../hooks/useVersionCheck'; import { useCredentialsSettings } from '../../../hooks/useCredentialsSettings'; import ApiKeysSection from './sections/ApiKeysSection'; import GithubCredentialsSection from './sections/GithubCredentialsSection'; import NewApiKeyAlert from './sections/NewApiKeyAlert'; -import VersionInfoSection from './sections/VersionInfoSection'; export default function CredentialsSettingsTab() { const { t } = useTranslation('settings'); - const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui'); const { apiKeys, githubCredentials, @@ -89,12 +86,6 @@ export default function CredentialsSettingsTab() { onDeleteGithubCredential={deleteGithubCredential} /> - ); } diff --git a/src/components/settings/view/tabs/api-settings/sections/VersionInfoSection.tsx b/src/components/settings/view/tabs/api-settings/sections/VersionInfoSection.tsx index 01b1fa2e..a7d5f83b 100644 --- a/src/components/settings/view/tabs/api-settings/sections/VersionInfoSection.tsx +++ b/src/components/settings/view/tabs/api-settings/sections/VersionInfoSection.tsx @@ -1,7 +1,29 @@ -import { ExternalLink } from 'lucide-react'; +import { ExternalLink, Star, MessageSquare } from 'lucide-react'; import { useTranslation } from 'react-i18next'; +import { IS_PLATFORM } from '../../../../../../constants/config'; import type { ReleaseInfo } from '../../../../../../types/sharedTypes'; +const GITHUB_REPO_URL = 'https://github.com/siteboon/claudecodeui'; +const DISCORD_URL = 'https://discord.gg/buxwujPNRE'; +const DOCS_URL = 'https://cloudcli.ai/docs/plugin-overview'; +const CLOUDCLI_URL = 'https://cloudcli.ai'; + +function GitHubIcon({ className }: { className?: string }) { + return ( + + ); +} + +function DiscordIcon({ className }: { className?: string }) { + return ( + + ); +} + type VersionInfoSectionProps = { currentVersion: string; updateAvailable: boolean; @@ -16,29 +38,115 @@ export default function VersionInfoSection({ releaseInfo, }: VersionInfoSectionProps) { const { t } = useTranslation('settings'); - const releasesUrl = releaseInfo?.htmlUrl || 'https://github.com/siteboon/claudecodeui/releases'; + const releasesUrl = releaseInfo?.htmlUrl || `${GITHUB_REPO_URL}/releases`; return (
-
+ {/* About CloudCLI */} +
+ {/* Logo + name + version */} +
+
+ +
+
+ +

+ Open-source AI coding assistant interface +

+
+
+ + {/* Star on GitHub button */} - v{currentVersion} + + + Star on GitHub - {updateAvailable && latestVersion && ( + + {/* Links */} + + + {/* Hosted CTA (OSS mode only) */} + {!IS_PLATFORM && ( +
+

Try CloudCLI Hosted

+

+ Team collaboration, shared MCP configs, settings sync across environments, and managed infrastructure. +

+ + Learn more + + +
)}
diff --git a/src/components/sidebar/view/Sidebar.tsx b/src/components/sidebar/view/Sidebar.tsx index cba08749..fefb6dca 100644 --- a/src/components/sidebar/view/Sidebar.tsx +++ b/src/components/sidebar/view/Sidebar.tsx @@ -266,6 +266,7 @@ function Sidebar({ updateAvailable={updateAvailable} releaseInfo={releaseInfo} latestVersion={latestVersion} + currentVersion={currentVersion} onShowVersionModal={() => setShowVersionModal(true)} onShowSettings={onShowSettings} projectListProps={projectListProps} diff --git a/src/components/sidebar/view/subcomponents/GitHubStarBadge.tsx b/src/components/sidebar/view/subcomponents/GitHubStarBadge.tsx new file mode 100644 index 00000000..539710c4 --- /dev/null +++ b/src/components/sidebar/view/subcomponents/GitHubStarBadge.tsx @@ -0,0 +1,48 @@ +import { Star, X } from 'lucide-react'; +import { useGitHubStars } from '../../../../hooks/useGitHubStars'; +import { IS_PLATFORM } from '../../../../constants/config'; + +const GITHUB_REPO_URL = 'https://github.com/siteboon/claudecodeui'; + +function GitHubIcon({ className }: { className?: string }) { + return ( + + ); +} + +export default function GitHubStarBadge() { + const { formattedCount, isDismissed, dismiss } = useGitHubStars('siteboon', 'claudecodeui'); + + if (IS_PLATFORM || isDismissed) return null; + + return ( +
+ + + + Star + {formattedCount && ( + {formattedCount} + )} + + +
+ ); +} diff --git a/src/components/sidebar/view/subcomponents/SidebarCollapsed.tsx b/src/components/sidebar/view/subcomponents/SidebarCollapsed.tsx index e09ceb87..90a6338f 100644 --- a/src/components/sidebar/view/subcomponents/SidebarCollapsed.tsx +++ b/src/components/sidebar/view/subcomponents/SidebarCollapsed.tsx @@ -1,7 +1,8 @@ -import { Settings, Sparkles, PanelLeftOpen } from 'lucide-react'; +import { Settings, Sparkles, PanelLeftOpen, Bug } from 'lucide-react'; import type { TFunction } from 'i18next'; const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE'; +const GITHUB_ISSUES_URL = 'https://github.com/siteboon/claudecodeui/issues/new'; function DiscordIcon({ className }: { className?: string }) { return ( @@ -50,6 +51,18 @@ export default function SidebarCollapsed({ + {/* Report Issue */} + + + + {/* Discord */} void; onShowSettings: () => void; projectListProps: SidebarProjectListProps; @@ -83,6 +84,7 @@ export default function SidebarContent({ updateAvailable, releaseInfo, latestVersion, + currentVersion, onShowVersionModal, onShowSettings, projectListProps, @@ -217,6 +219,7 @@ export default function SidebarContent({ updateAvailable={updateAvailable} releaseInfo={releaseInfo} latestVersion={latestVersion} + currentVersion={currentVersion} onShowVersionModal={onShowVersionModal} onShowSettings={onShowSettings} t={t} diff --git a/src/components/sidebar/view/subcomponents/SidebarFooter.tsx b/src/components/sidebar/view/subcomponents/SidebarFooter.tsx index afa0c6a1..b024be30 100644 --- a/src/components/sidebar/view/subcomponents/SidebarFooter.tsx +++ b/src/components/sidebar/view/subcomponents/SidebarFooter.tsx @@ -1,7 +1,11 @@ -import { Settings, ArrowUpCircle } from 'lucide-react'; +import { Settings, ArrowUpCircle, Bug } from 'lucide-react'; import type { TFunction } from 'i18next'; +import { IS_PLATFORM } from '../../../../constants/config'; import type { ReleaseInfo } from '../../../../types/sharedTypes'; +const GITHUB_ISSUES_URL = 'https://github.com/siteboon/claudecodeui/issues/new'; +const GITHUB_REPO_URL = 'https://github.com/siteboon/claudecodeui'; + const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE'; function DiscordIcon({ className }: { className?: string }) { @@ -16,6 +20,7 @@ type SidebarFooterProps = { updateAvailable: boolean; releaseInfo: ReleaseInfo | null; latestVersion: string | null; + currentVersion: string; onShowVersionModal: () => void; onShowSettings: () => void; t: TFunction; @@ -25,6 +30,7 @@ export default function SidebarFooter({ updateAvailable, releaseInfo, latestVersion, + currentVersion, onShowVersionModal, onShowSettings, t, @@ -79,11 +85,24 @@ export default function SidebarFooter({ )} - {/* Discord + Settings */} + {/* Community + Settings */}
- {/* Desktop Discord */} + {/* Desktop Report Issue */} + + {/* Desktop Discord */} + - {/* Mobile Discord */} + {/* Desktop version brand line (OSS mode only) */} + {!IS_PLATFORM && ( + + )} + + {/* Mobile Report Issue */} + + {/* Mobile Discord */} + + + {/* Search bar */} {projectsCount > 0 && !isLoading && (
diff --git a/src/components/version-upgrade/view/VersionUpgradeModal.tsx b/src/components/version-upgrade/view/VersionUpgradeModal.tsx index d4570ee5..cf6b94b7 100644 --- a/src/components/version-upgrade/view/VersionUpgradeModal.tsx +++ b/src/components/version-upgrade/view/VersionUpgradeModal.tsx @@ -1,9 +1,10 @@ -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { authenticatedFetch } from "../../../utils/api"; import { ReleaseInfo } from "../../../types/sharedTypes"; import { copyTextToClipboard } from "../../../utils/clipboard"; import type { InstallMode } from "../../../hooks/useVersionCheck"; +import { IS_PLATFORM } from "../../../constants/config"; interface VersionUpgradeModalProps { isOpen: boolean; @@ -14,6 +15,8 @@ interface VersionUpgradeModalProps { installMode: InstallMode; } +const RELOAD_COUNTDOWN_START = 30; + export function VersionUpgradeModal({ isOpen, onClose, @@ -25,14 +28,36 @@ export function VersionUpgradeModal({ const { t } = useTranslation('common'); const upgradeCommand = installMode === 'npm' ? t('versionUpdate.npmUpgradeCommand') - : 'git checkout main && git pull && npm install'; + : IS_PLATFORM + ? 'npm run update:platform' + : 'git checkout main && git pull && npm install'; const [isUpdating, setIsUpdating] = useState(false); const [updateOutput, setUpdateOutput] = useState(''); const [updateError, setUpdateError] = useState(''); + const [reloadCountdown, setReloadCountdown] = useState(null); + + useEffect(() => { + if (!IS_PLATFORM || reloadCountdown === null || reloadCountdown <= 0) { + return; + } + + const timeoutId = window.setTimeout(() => { + setReloadCountdown((previousCountdown) => { + if (previousCountdown === null) { + return null; + } + + return Math.max(previousCountdown - 1, 0); + }); + }, 1000); + + return () => window.clearTimeout(timeoutId); + }, [reloadCountdown]); const handleUpdateNow = useCallback(async () => { setIsUpdating(true); setUpdateOutput('Starting update...\n'); + setReloadCountdown(IS_PLATFORM ? RELOAD_COUNTDOWN_START : null); setUpdateError(''); try { @@ -46,7 +71,7 @@ export function VersionUpgradeModal({ if (response.ok) { setUpdateOutput(prev => prev + data.output + '\n'); setUpdateOutput(prev => prev + '\n✅ Update completed successfully!\n'); - setUpdateOutput(prev => prev + 'Please restart the server to apply changes.\n'); + setUpdateOutput(prev => prev + 'Please restart the server to apply changes.' + '\n'); } else { setUpdateError(data.error || 'Update failed'); setUpdateOutput(prev => prev + '\n❌ Update failed: ' + (data.error || 'Unknown error') + '\n'); @@ -143,6 +168,13 @@ export function VersionUpgradeModal({
{updateOutput}
+ {IS_PLATFORM && reloadCountdown !== null && ( +
+ {reloadCountdown === 0 + ? 'Refresh the page now. If that doesn\'t work, RESTART the environment.' + : `Refresh the page in ${reloadCountdown} ${reloadCountdown === 1 ? 'second' : 'seconds'}. If that doesn\'t work, RESTART the environment.`} +
+ )} {updateError && (
{updateError} diff --git a/src/hooks/useGitHubStars.ts b/src/hooks/useGitHubStars.ts new file mode 100644 index 00000000..2e731817 --- /dev/null +++ b/src/hooks/useGitHubStars.ts @@ -0,0 +1,77 @@ +import { useState, useEffect, useCallback } from 'react'; + +const CACHE_KEY = 'CLOUDCLI_GITHUB_STARS'; +const DISMISS_KEY = 'CLOUDCLI_HIDE_GITHUB_STAR'; +const CACHE_TTL = 60 * 60 * 1000; // 1 hour + +type CachedStars = { + count: number; + timestamp: number; +}; + +export const useGitHubStars = (owner: string, repo: string) => { + const [starCount, setStarCount] = useState(null); + const [isDismissed, setIsDismissed] = useState(() => { + try { + return localStorage.getItem(DISMISS_KEY) === 'true'; + } catch { + return false; + } + }); + + useEffect(() => { + if (isDismissed) return; + + // Check cache first + try { + const cached = localStorage.getItem(CACHE_KEY); + if (cached) { + const parsed: CachedStars = JSON.parse(cached); + if (Date.now() - parsed.timestamp < CACHE_TTL) { + setStarCount(parsed.count); + return; + } + } + } catch { + // ignore + } + + const fetchStars = async () => { + try { + const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`); + if (!response.ok) return; + const data = await response.json(); + const count = data.stargazers_count; + if (typeof count === 'number') { + setStarCount(count); + try { + localStorage.setItem(CACHE_KEY, JSON.stringify({ count, timestamp: Date.now() })); + } catch { + // ignore + } + } + } catch { + // silent fail + } + }; + + void fetchStars(); + }, [owner, repo, isDismissed]); + + const dismiss = useCallback(() => { + setIsDismissed(true); + try { + localStorage.setItem(DISMISS_KEY, 'true'); + } catch { + // ignore + } + }, []); + + const formattedCount = starCount !== null + ? starCount >= 1000 + ? `${(starCount / 1000).toFixed(1)}k` + : `${starCount}` + : null; + + return { starCount, formattedCount, isDismissed, dismiss }; +}; diff --git a/src/i18n/locales/de/auth.json b/src/i18n/locales/de/auth.json index 9b8327cb..0a1d5226 100644 --- a/src/i18n/locales/de/auth.json +++ b/src/i18n/locales/de/auth.json @@ -1,7 +1,7 @@ { "login": { "title": "Willkommen zurück", - "description": "Meld dich bei deinem Claude Code UI-Konto an", + "description": "Meld dich bei deinem CloudCLI-Konto an", "username": "Benutzername", "password": "Passwort", "submit": "Anmelden", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 1b1d4c7d..bc4c7473 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -84,7 +84,7 @@ "openInEditor": "Im Editor öffnen" }, "mainContent": { - "loading": "Claude Code UI wird geladen", + "loading": "CloudCLI wird geladen", "settingUpWorkspace": "Arbeitsbereich wird eingerichtet...", "chooseProject": "Projekt auswählen", "selectProjectDescription": "Wähl ein Projekt aus der Seitenleiste, um mit Claude zu programmieren. Jedes Projekt enthält deine Chat-Sitzungen und den Dateiverlauf.", diff --git a/src/i18n/locales/de/settings.json b/src/i18n/locales/de/settings.json index 25c289dd..6358c708 100644 --- a/src/i18n/locales/de/settings.json +++ b/src/i18n/locales/de/settings.json @@ -55,8 +55,7 @@ "appearance": "Darstellung", "toolDisplay": "Werkzeuganzeige", "viewOptions": "Anzeigeoptionen", - "inputSettings": "Eingabeeinstellungen", - "whisperDictation": "Whisper-Diktat" + "inputSettings": "Eingabeeinstellungen" }, "darkMode": "Darkmode", "autoExpandTools": "Werkzeuge automatisch erweitern", @@ -71,16 +70,6 @@ "openPanel": "Einstellungspanel öffnen", "draggingStatus": "Wird gezogen...", "toggleAndMove": "Klicken zum Umschalten, ziehen zum Verschieben" - }, - "whisper": { - "modes": { - "default": "Standardmodus", - "defaultDescription": "Direkte Transkription deiner Sprache", - "prompt": "Prompt-Verbesserung", - "promptDescription": "Rohe Ideen in klare, detaillierte KI-Prompts umwandeln", - "vibe": "Vibe-Modus", - "vibeDescription": "Ideen als klare Agentenanweisungen mit Details formatieren" - } } }, "terminalShortcuts": { @@ -105,7 +94,8 @@ "git": "Git", "apiTokens": "API & Token", "tasks": "Aufgaben", - "plugins": "Plugins" + "plugins": "Plugins", + "about": "Info" }, "appearanceSettings": { "darkMode": { diff --git a/src/i18n/locales/de/sidebar.json b/src/i18n/locales/de/sidebar.json index 8727f103..8b26756c 100644 --- a/src/i18n/locales/de/sidebar.json +++ b/src/i18n/locales/de/sidebar.json @@ -20,7 +20,7 @@ "runClaudeCli": "Führ Claude CLI in einem Projektverzeichnis aus, um zu beginnen" }, "app": { - "title": "Claude Code UI", + "title": "CloudCLI", "subtitle": "KI-Programmierassistent-Oberfläche" }, "sessions": { @@ -65,7 +65,12 @@ "save": "Speichern", "delete": "Löschen", "rename": "Umbenennen", - "joinCommunity": "Community beitreten" + "joinCommunity": "Community beitreten", + "reportIssue": "Problem melden", + "starOnGithub": "Stern auf GitHub" + }, + "branding": { + "openSource": "Open Source" }, "status": { "active": "Aktiv", diff --git a/src/i18n/locales/en/auth.json b/src/i18n/locales/en/auth.json index 2788855f..5ca25fd2 100644 --- a/src/i18n/locales/en/auth.json +++ b/src/i18n/locales/en/auth.json @@ -1,7 +1,7 @@ { "login": { "title": "Welcome Back", - "description": "Sign in to your Claude Code UI account", + "description": "Sign in to your CloudCLI self-hosted account", "username": "Username", "password": "Password", "submit": "Sign In", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 928bdaf1..0d25fedf 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -84,7 +84,7 @@ "openInEditor": "Open in Editor" }, "mainContent": { - "loading": "Loading Claude Code UI", + "loading": "Loading CloudCLI", "settingUpWorkspace": "Setting up your workspace...", "chooseProject": "Choose Your Project", "selectProjectDescription": "Select a project from the sidebar to start coding with Claude. Each project contains your chat sessions and file history.", diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 8596e045..1285ac3f 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -55,8 +55,7 @@ "appearance": "Appearance", "toolDisplay": "Tool Display", "viewOptions": "View Options", - "inputSettings": "Input Settings", - "whisperDictation": "Whisper Dictation" + "inputSettings": "Input Settings" }, "darkMode": "Dark Mode", "autoExpandTools": "Auto-expand tools", @@ -71,16 +70,6 @@ "openPanel": "Open settings panel", "draggingStatus": "Dragging...", "toggleAndMove": "Click to toggle, drag to move" - }, - "whisper": { - "modes": { - "default": "Default Mode", - "defaultDescription": "Direct transcription of your speech", - "prompt": "Prompt Enhancement", - "promptDescription": "Transform rough ideas into clear, detailed AI prompts", - "vibe": "Vibe Mode", - "vibeDescription": "Format ideas as clear agent instructions with details" - } } }, "terminalShortcuts": { @@ -106,8 +95,8 @@ "apiTokens": "API & Tokens", "tasks": "Tasks", "notifications": "Notifications", - "plugins": "Plugins" - + "plugins": "Plugins", + "about": "About" }, "notifications": { "title": "Notifications", @@ -498,4 +487,4 @@ "tab": "tab", "runningStatus": "running" } -} \ No newline at end of file +} diff --git a/src/i18n/locales/en/sidebar.json b/src/i18n/locales/en/sidebar.json index a56c1b84..eab0a3f3 100644 --- a/src/i18n/locales/en/sidebar.json +++ b/src/i18n/locales/en/sidebar.json @@ -20,7 +20,7 @@ "runClaudeCli": "Run Claude CLI in a project directory to get started" }, "app": { - "title": "Claude Code UI", + "title": "CloudCLI", "subtitle": "AI coding assistant interface" }, "sessions": { @@ -65,7 +65,12 @@ "save": "Save", "delete": "Delete", "rename": "Rename", - "joinCommunity": "Join Community" + "joinCommunity": "Join Community", + "reportIssue": "Report Issue", + "starOnGithub": "Star on GitHub" + }, + "branding": { + "openSource": "Open Source" }, "status": { "active": "Active", diff --git a/src/i18n/locales/ja/auth.json b/src/i18n/locales/ja/auth.json index ce946497..d1ba9fd8 100644 --- a/src/i18n/locales/ja/auth.json +++ b/src/i18n/locales/ja/auth.json @@ -1,7 +1,7 @@ { "login": { "title": "おかえりなさい", - "description": "Claude Code UIアカウントにサインイン", + "description": "CloudCLIアカウントにサインイン", "username": "ユーザー名", "password": "パスワード", "submit": "サインイン", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 59c33c98..097eb057 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -84,7 +84,7 @@ "openInEditor": "エディタで開く" }, "mainContent": { - "loading": "Claude Code UI を読み込んでいます", + "loading": "CloudCLI を読み込んでいます", "settingUpWorkspace": "ワークスペースを準備しています...", "chooseProject": "プロジェクトを選択", "selectProjectDescription": "サイドバーからプロジェクトを選択して、Claudeとコーディングを始めましょう。各プロジェクトにはチャットセッションとファイル履歴が含まれています。", diff --git a/src/i18n/locales/ja/settings.json b/src/i18n/locales/ja/settings.json index 60b454ca..53cec8d1 100644 --- a/src/i18n/locales/ja/settings.json +++ b/src/i18n/locales/ja/settings.json @@ -55,8 +55,7 @@ "appearance": "外観", "toolDisplay": "ツール表示", "viewOptions": "表示オプション", - "inputSettings": "入力設定", - "whisperDictation": "Whisper音声入力" + "inputSettings": "入力設定" }, "darkMode": "ダークモード", "autoExpandTools": "ツールを自動展開", @@ -71,16 +70,6 @@ "openPanel": "設定パネルを開く", "draggingStatus": "ドラッグ中...", "toggleAndMove": "クリックで切替、ドラッグで移動" - }, - "whisper": { - "modes": { - "default": "標準モード", - "defaultDescription": "音声をそのまま文字起こしします", - "prompt": "プロンプト強化", - "promptDescription": "ラフなアイデアを明確で詳細なAIプロンプトに変換します", - "vibe": "バイブモード", - "vibeDescription": "アイデアを明確なエージェント指示に整形します" - } } }, "terminalShortcuts": { @@ -106,8 +95,8 @@ "apiTokens": "API & トークン", "tasks": "タスク", "notifications": "通知", - "plugins": "プラグイン" - + "plugins": "プラグイン", + "about": "概要" }, "notifications": { "title": "通知", @@ -492,4 +481,4 @@ "tab": "タブ", "runningStatus": "実行中" } -} \ No newline at end of file +} diff --git a/src/i18n/locales/ja/sidebar.json b/src/i18n/locales/ja/sidebar.json index 17813411..4590c752 100644 --- a/src/i18n/locales/ja/sidebar.json +++ b/src/i18n/locales/ja/sidebar.json @@ -20,7 +20,7 @@ "runClaudeCli": "プロジェクトディレクトリでClaude CLIを実行して始めましょう" }, "app": { - "title": "Claude Code UI", + "title": "CloudCLI", "subtitle": "AIコーディングアシスタント" }, "sessions": { @@ -64,7 +64,12 @@ "save": "保存", "delete": "削除", "rename": "名前の変更", - "joinCommunity": "コミュニティに参加" + "joinCommunity": "コミュニティに参加", + "reportIssue": "問題を報告", + "starOnGithub": "GitHubでスター" + }, + "branding": { + "openSource": "オープンソース" }, "status": { "active": "アクティブ", diff --git a/src/i18n/locales/ko/auth.json b/src/i18n/locales/ko/auth.json index ebfef608..08f09e74 100644 --- a/src/i18n/locales/ko/auth.json +++ b/src/i18n/locales/ko/auth.json @@ -1,7 +1,7 @@ { "login": { "title": "다시 오신 것을 환영합니다", - "description": "Claude Code UI 계정에 로그인하세요", + "description": "CloudCLI 계정에 로그인하세요", "username": "사용자명", "password": "비밀번호", "submit": "로그인", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 95004280..b3554401 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -84,7 +84,7 @@ "openInEditor": "에디터에서 열기" }, "mainContent": { - "loading": "Claude Code UI 로딩 중", + "loading": "CloudCLI 로딩 중", "settingUpWorkspace": "워크스페이스 설정 중...", "chooseProject": "프로젝트 선택", "selectProjectDescription": "사이드바에서 프로젝트를 선택하여 Claude와 코딩을 시작하세요. 각 프로젝트에는 채팅 세션과 파일 히스토리가 포함됩니다.", diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json index b8a1f450..0d3d2d30 100644 --- a/src/i18n/locales/ko/settings.json +++ b/src/i18n/locales/ko/settings.json @@ -55,8 +55,7 @@ "appearance": "외관", "toolDisplay": "도구 표시", "viewOptions": "보기 옵션", - "inputSettings": "입력 설정", - "whisperDictation": "Whisper 음성 인식" + "inputSettings": "입력 설정" }, "darkMode": "다크 모드", "autoExpandTools": "도구 자동 펼치기", @@ -71,16 +70,6 @@ "openPanel": "설정 패널 열기", "draggingStatus": "드래그 중...", "toggleAndMove": "클릭하여 토글, 드래그하여 이동" - }, - "whisper": { - "modes": { - "default": "기본 모드", - "defaultDescription": "음성을 그대로 텍스트로 변환", - "prompt": "프롬프트 향상", - "promptDescription": "거친 아이디어를 명확하고 상세한 AI 프롬프트로 변환", - "vibe": "Vibe 모드", - "vibeDescription": "아이디어를 상세한 에이전트 지침 형식으로 변환" - } } }, "terminalShortcuts": { @@ -106,8 +95,8 @@ "apiTokens": "API & 토큰", "tasks": "작업", "notifications": "알림", - "plugins": "플러그인" - + "plugins": "플러그인", + "about": "정보" }, "notifications": { "title": "알림", @@ -492,4 +481,4 @@ "tab": "탭", "runningStatus": "실행 중" } -} \ No newline at end of file +} diff --git a/src/i18n/locales/ko/sidebar.json b/src/i18n/locales/ko/sidebar.json index 2eaca911..cda7eab2 100644 --- a/src/i18n/locales/ko/sidebar.json +++ b/src/i18n/locales/ko/sidebar.json @@ -20,7 +20,7 @@ "runClaudeCli": "프로젝트 디렉토리에서 Claude CLI를 실행하여 시작하세요" }, "app": { - "title": "Claude Code UI", + "title": "CloudCLI", "subtitle": "AI 코딩 어시스턴트 UI" }, "sessions": { @@ -64,7 +64,12 @@ "save": "저장", "delete": "삭제", "rename": "이름 변경", - "joinCommunity": "커뮤니티 참여" + "joinCommunity": "커뮤니티 참여", + "reportIssue": "문제 신고", + "starOnGithub": "GitHub에서 스타" + }, + "branding": { + "openSource": "오픈 소스" }, "status": { "active": "활성", diff --git a/src/i18n/locales/ru/auth.json b/src/i18n/locales/ru/auth.json index b81fd562..919a4a30 100644 --- a/src/i18n/locales/ru/auth.json +++ b/src/i18n/locales/ru/auth.json @@ -1,7 +1,7 @@ { "login": { "title": "Добро пожаловать", - "description": "Войдите в свой аккаунт Claude Code UI", + "description": "Войдите в свой аккаунт CloudCLI", "username": "Имя пользователя", "password": "Пароль", "submit": "Войти", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 665ab6f8..906b78eb 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -84,7 +84,7 @@ "openInEditor": "Открыть в редакторе" }, "mainContent": { - "loading": "Загрузка Claude Code UI", + "loading": "Загрузка CloudCLI", "settingUpWorkspace": "Настройка рабочего пространства...", "chooseProject": "Выберите проект", "selectProjectDescription": "Выберите проект на боковой панели, чтобы начать работу с Claude. Каждый проект содержит ваши сеансы чата и историю файлов.", diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index f8991ab0..98944876 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -55,8 +55,7 @@ "appearance": "Внешний вид", "toolDisplay": "Отображение инструментов", "viewOptions": "Параметры просмотра", - "inputSettings": "Настройки ввода", - "whisperDictation": "Диктовка Whisper" + "inputSettings": "Настройки ввода" }, "darkMode": "Темная тема", "autoExpandTools": "Автоматически разворачивать инструменты", @@ -71,16 +70,6 @@ "openPanel": "Открыть панель настроек", "draggingStatus": "Перетаскивание...", "toggleAndMove": "Нажмите для переключения, перетащите для перемещения" - }, - "whisper": { - "modes": { - "default": "Режим по умолчанию", - "defaultDescription": "Прямая транскрипция вашей речи", - "prompt": "Улучшение запроса", - "promptDescription": "Преобразование грубых идей в четкие, детальные AI-запросы", - "vibe": "Режим Vibe", - "vibeDescription": "Форматирование идей как четких инструкций агента с деталями" - } } }, "terminalShortcuts": { @@ -105,7 +94,8 @@ "git": "Git", "apiTokens": "API и токены", "tasks": "Задачи", - "plugins": "Плагины" + "plugins": "Плагины", + "about": "О программе" }, "appearanceSettings": { "darkMode": { @@ -471,4 +461,4 @@ "tab": "вкладка", "runningStatus": "запущен" } -} \ No newline at end of file +} diff --git a/src/i18n/locales/ru/sidebar.json b/src/i18n/locales/ru/sidebar.json index d79a9d3d..71250d2f 100644 --- a/src/i18n/locales/ru/sidebar.json +++ b/src/i18n/locales/ru/sidebar.json @@ -20,7 +20,7 @@ "runClaudeCli": "Запустите Claude CLI в каталоге проекта для начала работы" }, "app": { - "title": "Claude Code UI", + "title": "CloudCLI", "subtitle": "Интерфейс AI помощника для программирования" }, "sessions": { @@ -65,7 +65,12 @@ "save": "Сохранить", "delete": "Удалить", "rename": "Переименовать", - "joinCommunity": "Присоединиться к сообществу" + "joinCommunity": "Присоединиться к сообществу", + "reportIssue": "Сообщить о проблеме", + "starOnGithub": "Звезда на GitHub" + }, + "branding": { + "openSource": "Открытый исходный код" }, "status": { "active": "Активен", diff --git a/src/i18n/locales/zh-CN/auth.json b/src/i18n/locales/zh-CN/auth.json index 3905c2bd..3d790f37 100644 --- a/src/i18n/locales/zh-CN/auth.json +++ b/src/i18n/locales/zh-CN/auth.json @@ -1,7 +1,7 @@ { "login": { "title": "欢迎回来", - "description": "登录您的 Claude Code UI 账户", + "description": "登录您的 CloudCLI 账户", "username": "用户名", "password": "密码", "submit": "登录", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 710daf5e..936fe5b2 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -84,7 +84,7 @@ "openInEditor": "在编辑器中打开" }, "mainContent": { - "loading": "正在加载 Claude Code UI", + "loading": "正在加载 CloudCLI", "settingUpWorkspace": "正在设置您的工作空间...", "chooseProject": "选择您的项目", "selectProjectDescription": "从侧边栏选择一个项目以开始使用 Claude 进行编程。每个项目包含您的聊天会话和文件历史。", diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index d9f2b2cd..cfcc8ee2 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -55,8 +55,7 @@ "appearance": "外观", "toolDisplay": "工具显示", "viewOptions": "视图选项", - "inputSettings": "输入设置", - "whisperDictation": "Whisper 听写" + "inputSettings": "输入设置" }, "darkMode": "深色模式", "autoExpandTools": "自动展开工具", @@ -71,16 +70,6 @@ "openPanel": "打开设置面板", "draggingStatus": "正在拖拽...", "toggleAndMove": "点击切换,拖拽移动" - }, - "whisper": { - "modes": { - "default": "默认模式", - "defaultDescription": "直接转录您的语音", - "prompt": "提示词增强", - "promptDescription": "将粗略的想法转化为清晰、详细的 AI 提示词", - "vibe": "Vibe 模式", - "vibeDescription": "将想法格式化为带有详细说明的清晰智能体指令" - } } }, "terminalShortcuts": { @@ -106,8 +95,8 @@ "apiTokens": "API 和令牌", "tasks": "任务", "notifications": "通知", - "plugins": "插件" - + "plugins": "插件", + "about": "关于" }, "notifications": { "title": "通知", @@ -492,4 +481,4 @@ "tab": "标签", "runningStatus": "运行中" } -} \ No newline at end of file +} diff --git a/src/i18n/locales/zh-CN/sidebar.json b/src/i18n/locales/zh-CN/sidebar.json index 27cacec5..3a28778c 100644 --- a/src/i18n/locales/zh-CN/sidebar.json +++ b/src/i18n/locales/zh-CN/sidebar.json @@ -20,7 +20,7 @@ "runClaudeCli": "在项目目录中运行 Claude CLI 以开始使用" }, "app": { - "title": "Claude Code UI", + "title": "CloudCLI", "subtitle": "AI 编程助手" }, "sessions": { @@ -65,7 +65,12 @@ "save": "保存", "delete": "删除", "rename": "重命名", - "joinCommunity": "加入社区" + "joinCommunity": "加入社区", + "reportIssue": "报告问题", + "starOnGithub": "在GitHub上加星" + }, + "branding": { + "openSource": "开源" }, "status": { "active": "活动", diff --git a/src/utils/api.js b/src/utils/api.js index 7c14a677..438cab82 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -147,13 +147,6 @@ export const api = { headers: {}, // Let browser set Content-Type for FormData }), - transcribe: (formData) => - authenticatedFetch('/api/transcribe', { - method: 'POST', - body: formData, - headers: {}, // Let browser set Content-Type for FormData - }), - // TaskMaster endpoints taskmaster: { // Initialize TaskMaster in a project