Compare commits

..

2 Commits

Author SHA1 Message Date
Haileyesus
58f7247a2d fix: show CTRL+K correctly in chatview 2026-06-05 09:48:12 +03:00
Haileyesus
e2ff79bb82 chore: update claude agent sdk to latest version 2026-06-05 09:45:15 +03:00
54 changed files with 928 additions and 3003 deletions

View File

@@ -3,35 +3,6 @@
All notable changes to CloudCLI UI will be documented in this file. All notable changes to CloudCLI UI will be documented in this file.
## [1.33.1](https://github.com/siteboon/claudecodeui/compare/v1.33.0...v1.33.1) (2026-06-05)
### New Features
* **chat:** auto-detect text direction for RTL languages ([#729](https://github.com/siteboon/claudecodeui/issues/729)) ([fa9eaf5](https://github.com/siteboon/claudecodeui/commit/fa9eaf5573a6f870a19fb62ab430ffd87c466582))
### Bug Fixes
* file tree concurrency ([#828](https://github.com/siteboon/claudecodeui/issues/828)) ([ebb0e59](https://github.com/siteboon/claudecodeui/commit/ebb0e59e8023c0a8040d168a5adffb7102e80561))
* load claude models directly from provider ([cdcac18](https://github.com/siteboon/claudecodeui/commit/cdcac182d458a24908777568979c8e756f94428c))
* plugin svg icon sanitization ([#817](https://github.com/siteboon/claudecodeui/issues/817)) ([d9e9df1](https://github.com/siteboon/claudecodeui/commit/d9e9df183f462c88c3b60975eb8254faa9168717))
* recognize claude auth token env ([#818](https://github.com/siteboon/claudecodeui/issues/818)) ([43c33d5](https://github.com/siteboon/claudecodeui/commit/43c33d5cb1b41835dfe3bccd450c5a9c2441509b))
* redact websocket auth token in logs ([#827](https://github.com/siteboon/claudecodeui/issues/827)) ([14ddbc7](https://github.com/siteboon/claudecodeui/commit/14ddbc7c57a01da9fb65fd87d8588532b11833fa))
* remove thinking mode ([#835](https://github.com/siteboon/claudecodeui/issues/835)) ([2149b87](https://github.com/siteboon/claudecodeui/commit/2149b8776b7ebfec0eace413f4fc527ccb2324c0))
* **shell:** disconnect and restart buttons ([#831](https://github.com/siteboon/claudecodeui/issues/831)) ([ef2fd48](https://github.com/siteboon/claudecodeui/commit/ef2fd48b46452d4b9e2bf1f5e3c30fafe19f27f2))
* show Claude tool result errors ([bb8db58](https://github.com/siteboon/claudecodeui/commit/bb8db5815c2d20ee4fbfa02d14c886a56ef352e0))
* **vite:** proxy /plugin-ws WebSocket requests to the backend in dev ([#757](https://github.com/siteboon/claudecodeui/issues/757)) ([96b16b4](https://github.com/siteboon/claudecodeui/commit/96b16b42e4f807d04ec743a5a4117a37a3f5e0d9))
* **websocket:** add 30s server-side heartbeat to prevent proxy idle disconnects ([#770](https://github.com/siteboon/claudecodeui/issues/770)) ([2edfef2](https://github.com/siteboon/claudecodeui/commit/2edfef2e3f4271c29ae8670df9dd382a9eef7c3c)), closes [#769](https://github.com/siteboon/claudecodeui/issues/769)
* **websocket:** reset unmountedRef on each effect re-run so token refresh reconnects ([#721](https://github.com/siteboon/claudecodeui/issues/721)) ([f082cdc](https://github.com/siteboon/claudecodeui/commit/f082cdc63bd0de90f8b3da1df6071e91ab545831))
### Documentation
* add nginx subpath deployment template ([#820](https://github.com/siteboon/claudecodeui/issues/820)) ([3ec76b5](https://github.com/siteboon/claudecodeui/commit/3ec76b5bb15a13cec41056f4c9b9c425195022fa))
### Maintenance
* update Claude fallback models ([94785bf](https://github.com/siteboon/claudecodeui/commit/94785bfa579d1f39a2bee0f9dd0f09fd0243bc79))
* update package-lock.json ([c90b341](https://github.com/siteboon/claudecodeui/commit/c90b34108e86a3effdb5c6979ea7b1692d2b9da0))
## [](https://github.com/siteboon/claudecodeui/compare/v1.32.0...vnull) (2026-06-01) ## [](https://github.com/siteboon/claudecodeui/compare/v1.32.0...vnull) (2026-06-01)
### New Features ### New Features

View File

@@ -15,7 +15,7 @@
<a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p> </p>
<div align="right"><i><a href="./README.md">English</a> · <a href="./README.ru.md">Русский</a> · <b>Deutsch</b> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.tr.md">Türkçe</a></i></div> <div align="right"><i><a href="./README.md">English</a> · <a href="./README.ru.md">Русский</a> · <b>Deutsch</b> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.tr.md">Türkçe</a></i></div>
--- ---

View File

@@ -15,7 +15,7 @@
<a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p> </p>
<div align="right"><i><a href="./README.md">English</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <b>日本語</b> · <a href="./README.tr.md">Türkçe</a></i></div> <div align="right"><i><a href="./README.md">English</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">中文</a> · <b>日本語</b> · <a href="./README.tr.md">Türkçe</a></i></div>
--- ---

View File

@@ -15,7 +15,7 @@
<a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p> </p>
<div align="right"><i><a href="./README.md">English</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.de.md">Deutsch</a> · <b>한국어</b> · <a href="./README.zh-CN.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.tr.md">Türkçe</a></i></div> <div align="right"><i><a href="./README.md">English</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.de.md">Deutsch</a> · <b>한국어</b> · <a href="./README.zh-CN.md">中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.tr.md">Türkçe</a></i></div>
--- ---

View File

@@ -15,7 +15,7 @@
<a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p> </p>
<div align="right"><i><b>English</b> · <a href="./README.ru.md">Русский</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.tr.md">Türkçe</a></i></div> <div align="right"><i><b>English</b> · <a href="./README.ru.md">Русский</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.tr.md">Türkçe</a></i></div>
--- ---

View File

@@ -15,7 +15,7 @@
<a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p> </p>
<div align="right"><i><a href="./README.md">English</a> · <b>Русский</b> · <a href="./README.de.md">Deutsch</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.tr.md">Türkçe</a></i></div> <div align="right"><i><a href="./README.md">English</a> · <b>Русский</b> · <a href="./README.de.md">Deutsch</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.tr.md">Türkçe</a></i></div>
--- ---

View File

@@ -15,7 +15,7 @@
<a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p> </p>
<div align="right"><i><a href="./README.md">English</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <b>Türkçe</b></i></div> <div align="right"><i><a href="./README.md">English</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">中文</a> · <a href="./README.ja.md">日本語</a> · <b>Türkçe</b></i></div>
--- ---

View File

@@ -15,7 +15,7 @@
<a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p> </p>
<div align="right"><i><a href="./README.md">English</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.ko.md">한국어</a> · <b>简体中文</b> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.tr.md">Türkçe</a></i></div> <div align="right"><i><a href="./README.md">English</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.ko.md">한국어</a> · <b>中文</b> · <a href="./README.ja.md">日本語</a> · <a href="./README.tr.md">Türkçe</a></i></div>
--- ---

View File

@@ -1,242 +0,0 @@
<div align="center">
<img src="public/logo.svg" alt="CloudCLI UI" width="64" height="64">
<h1>Cloud CLI又名 Claude Code UI</h1>
<p><a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a><a href="https://docs.cursor.com/en/cli/overview">Cursor CLI</a><a href="https://developers.openai.com/codex">Codex</a><a href="https://geminicli.com/">Gemini-CLI</a> 的桌面和行動裝置 UI。可在本機或遠端使用從任何地方查看您的專案與工作階段。</p>
</div>
<p align="center">
<a href="https://cloudcli.ai">CloudCLI Cloud</a> · <a href="https://cloudcli.ai/docs">文件</a> · <a href="https://discord.gg/buxwujPNRE">Discord</a> · <a href="https://github.com/siteboon/claudecodeui/issues">Bug 回報</a> · <a href="CONTRIBUTING.md">貢獻指南</a>
</p>
<p align="center">
<a href="https://cloudcli.ai"><img src="https://img.shields.io/badge/☁_CloudCLI_Cloud-Try_Now-0066FF?style=for-the-badge" alt="CloudCLI Cloud"></a>
<a href="https://discord.gg/buxwujPNRE"><img src="https://img.shields.io/badge/Discord-Join%20Community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="加入 Discord 社群"></a>
<br><br>
<a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p>
<div align="right"><i><a href="./README.md">English</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">简体中文</a> · <b>繁體中文</b> · <a href="./README.ja.md">日本語</a> · <a href="./README.tr.md">Türkçe</a></i></div>
---
## 截圖
<div align="center">
<table>
<tr>
<td align="center">
<h3>桌面檢視</h3>
<img src="public/screenshots/desktop-main.png" alt="桌面介面" width="400">
<br>
<em>顯示專案總覽和聊天的主介面</em>
</td>
<td align="center">
<h3>行動裝置體驗</h3>
<img src="public/screenshots/mobile-chat.png" alt="行動裝置介面" width="250">
<br>
<em>具有觸控導覽的響應式行動裝置設計</em>
</td>
</tr>
<tr>
<td align="center" colspan="2">
<h3>CLI 選擇</h3>
<img src="public/screenshots/cli-selection.png" alt="CLI 選擇" width="400">
<br>
<em>在 Claude Code、Gemini、Cursor CLI 與 Codex 之間進行選擇</em>
</td>
</tr>
</table>
</div>
## 功能
- **響應式設計** — 在桌面、平板和行動裝置上無縫運作,讓您隨時隨地使用 Agents
- **互動聊天介面** — 內建聊天 UI輕鬆與 Agents 交流
- **整合 Shell 終端機** — 透過內建 shell 功能直接存取 Agents CLI
- **檔案瀏覽器** — 互動式檔案樹,支援語法醒目提示與即時編輯
- **Git 瀏覽器** — 檢視、暫存並提交變更,還可切換分支
- **工作階段管理** — 恢復對話、管理多個工作階段並追蹤歷史紀錄
- **外掛系統** — 透過自訂分頁、後端服務與整合來擴充 CloudCLI。[開始建構 →](https://github.com/cloudcli-ai/cloudcli-plugin-starter)
- **TaskMaster AI 整合** *(選用)* — 結合 AI 任務規劃、PRD 分析與工作流程自動化,實現進階專案管理
- **模型相容性** — 支援 Claude、GPT、Gemini 模型家族(完整支援列表見 [`shared/modelConstants.js`](shared/modelConstants.js)
## 快速開始
### CloudCLI Cloud推薦
無需本機設定即可快速啟動。提供可透過網路瀏覽器、行動應用程式、API 或慣用的 IDE 存取的完全容器化託管開發環境。
**[立即開始 CloudCLI Cloud](https://cloudcli.ai)**
### 自架(開源)
#### npm
啟動 CloudCLI UI只需一行 `npx`(需要 Node.js v22+
```bash
npx @cloudcli-ai/cloudcli
```
或進行全域安裝,便於日常使用:
```bash
npm install -g @cloudcli-ai/cloudcli
cloudcli
```
開啟 `http://localhost:3001`,系統會自動發現所有現有工作階段。
更多設定選項、PM2、遠端伺服器設定等請參閱 **[文件 →](https://cloudcli.ai/docs)**。
#### Docker Sandboxes實驗性
在隔離的沙箱中執行代理,具有虛擬機管理程式等級的隔離。預設啟動 Claude Code。需要 [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/)。
```bash
npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project
```
支援 Claude Code、Codex 和 Gemini CLI。詳情請參閱[沙箱文件](docker/)。
---
## 哪個選項更適合你?
CloudCLI UI 是 CloudCLI Cloud 的開源 UI 層。你可以在本機上自架它,也可以使用提供團隊功能與深入整合的 CloudCLI Cloud。
| | CloudCLI UI自架 | CloudCLI Cloud |
|---|---|---|
| **適合對象** | 需要為本機代理工作階段提供完整 UI 的開發者 | 需要部署在雲端,隨時從任何地方存取代理的團隊與開發者 |
| **存取方式** | 透過 `[yourip]:port` 在瀏覽器中存取 | 瀏覽器、任意 IDE、REST API、n8n |
| **設定** | `npx @cloudcli-ai/cloudcli` | 無需設定 |
| **機器需保持開機嗎** | 是 | 否 |
| **行動裝置存取** | 網路內任意瀏覽器 | 任意裝置(原生應用程式即將推出) |
| **可用工作階段** | 自動發現 `~/.claude` 中的所有工作階段 | 雲端環境內的工作階段 |
| **支援的 Agents** | Claude Code、Cursor CLI、Codex、Gemini CLI | Claude Code、Cursor CLI、Codex、Gemini CLI |
| **檔案瀏覽與 Git** | 內建於 UI | 內建於 UI |
| **MCP 設定** | UI 管理,與本機 `~/.claude` 設定同步 | UI 管理 |
| **IDE 存取** | 本機 IDE | 任何連線到雲端環境的 IDE |
| **REST API** | 是 | 是 |
| **n8n 節點** | 否 | 是 |
| **團隊共享** | 否 | 是 |
| **平台費用** | 免費開源 | 起價 $7/月 |
> 兩種方式都使用你自己的 AI 訂閱Claude、Cursor 等)— CloudCLI 提供環境,而非 AI。
---
## 安全與工具設定
**🔒 重要提示**:所有 Claude Code 工具預設**停用**,可防止潛在的有害操作自動執行。
### 啟用工具
1. **開啟工具設定** — 點擊側邊欄齒輪圖示
2. **選擇性啟用** — 僅啟用所需工具
3. **套用設定** — 偏好設定儲存在本機
<div align="center">
![工具設定彈出視窗](public/screenshots/tools-modal.png)
*工具設定介面 — 只啟用你需要的內容*
</div>
**建議做法**:先啟用基礎工具,再根據需要新增其他工具。隨時可以調整。
---
## 外掛
CloudCLI 配備外掛系統,允許你新增帶有自訂前端 UI 和選用 Node.js 後端的分頁。在 Settings > Plugins 中直接從 Git 儲存庫安裝外掛,或自行開發。
### 可用外掛
| 外掛 | 描述 |
|---|---|
| **[Project Stats](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** | 展示目前專案的檔案數、程式碼行數、檔案類型分佈、最大檔案以及最近修改的檔案 |
### 自行建構
**[Plugin Starter Template →](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** — Fork 該儲存庫以建構自己的外掛。範例包括前端渲染、即時上下文更新和 RPC 通訊。
**[外掛文件 →](https://cloudcli.ai/docs/plugin-overview)** — 提供外掛 API、清單格式、安全模型等完整指南。
---
## 常見問題
<details>
<summary>與 Claude Code Remote Control 有何不同?</summary>
Claude Code Remote Control 讓你傳送訊息到本機終端機中已經執行的工作階段。該方式要求你的機器保持開機,終端機保持開啟,中斷網路後約 10 分鐘工作階段會逾時。
CloudCLI UI 與 CloudCLI Cloud 是對 Claude Code 的擴充,而非旁觀 — MCP 伺服器、權限、設定、工作階段與 Claude Code 完全一致。
- **涵蓋全部工作階段** — CloudCLI UI 會自動掃描 `~/.claude` 資料夾中的每個工作階段。Remote Control 只暴露目前活動的工作階段。
- **設定統一** — 在 CloudCLI UI 中修改的 MCP、工具權限等設定會立即寫入 Claude Code。
- **支援更多 Agents** — Claude Code、Cursor CLI、Codex、Gemini CLI。
- **完整 UI** — 除了聊天介面還包括檔案瀏覽器、Git 整合、MCP 管理和 Shell 終端機。
- **CloudCLI Cloud 持續運作於雲端** — 關閉本機裝置也不會中斷代理執行,無需監控終端機。
</details>
<details>
<summary>需要額外購買 AI 訂閱嗎?</summary>
需要。CloudCLI 只提供環境。你仍需自行取得 Claude、Cursor、Codex 或 Gemini 訂閱。CloudCLI Cloud 從 $7/月起提供託管環境。
</details>
<details>
<summary>能在手機上使用 CloudCLI UI 嗎?</summary>
可以。自架時,在你的裝置上執行伺服器,然後在網路中的任意瀏覽器開啟 `[yourip]:port`。CloudCLI Cloud 可從任意裝置存取,內建原生應用程式也在開發中。
</details>
<details>
<summary>UI 中的變更會影響本機 Claude Code 設定嗎?</summary>
會的。自架模式下CloudCLI UI 讀取並寫入 Claude Code 使用的 `~/.claude` 設定。透過 UI 新增的 MCP 伺服器會立即在 Claude Code 中可見。
</details>
---
## 社群與支援
- **[文件](https://cloudcli.ai/docs)** — 安裝、設定、功能與疑難排解指南
- **[Discord](https://discord.gg/buxwujPNRE)** — 取得協助並與社群交流
- **[GitHub Issues](https://github.com/siteboon/claudecodeui/issues)** — 回報 Bug 與建議功能
- **[貢獻指南](CONTRIBUTING.md)** — 如何參與專案貢獻
## 授權條款
GNU 通用公共授權條款 v3.0 — 詳見 [LICENSE](LICENSE) 檔案。
該專案為開源軟體,在 GPL v3 授權條款下可自由使用、修改與散布。
## 致謝
### 使用技術
- **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** — Anthropic 官方 CLI
- **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** — Cursor 官方 CLI
- **[Codex](https://developers.openai.com/codex)** — OpenAI Codex
- **[Gemini-CLI](https://geminicli.com/)** — Google Gemini CLI
- **[React](https://react.dev/)** — 使用者介面函式庫
- **[Vite](https://vitejs.dev/)** — 快速建構工具與開發伺服器
- **[Tailwind CSS](https://tailwindcss.com/)** — 實用優先 CSS 框架
- **[CodeMirror](https://codemirror.net/)** — 進階程式碼編輯器
- **[TaskMaster AI](https://github.com/eyaltoledano/claude-task-master)** *(選用)* — AI 驅動的專案管理與任務規劃
### 贊助商
- [Siteboon - AI powered website builder](https://siteboon.ai)
---
<div align="center">
<strong>為 Claude Code、Cursor 和 Codex 社群精心打造。</strong>
</div>

View File

@@ -1,218 +0,0 @@
# CloudCLI UI Nginx subpath deployment template.
#
# Purpose:
# Serve CloudCLI UI from a path prefix such as:
# http://localhost/ai/
# https://example.com/ai/
#
# CloudCLI itself still runs at the root of its own HTTP server, for example:
# http://127.0.0.1:3001/
#
# Nginx receives public requests under /ai, strips that prefix, and forwards the
# remaining path to CloudCLI. For example:
# /ai/ -> /
# /ai/session/abc -> /session/abc
# /ai/assets/index.js -> /assets/index.js
#
# Important Nginx limitation:
# Nginx does not allow variables in `location` matchers or `rewrite` regexes.
# The configurable variables below are still useful for proxy/filter values,
# but if you change /ai to a different subpath, also update every line marked:
# [SUBPATH LITERAL]
#
# To use a different subpath, replace these literal matchers:
# location = /ai
# location ^~ /ai/
# rewrite ^/ai(?<cloudcli_path>/.*)$ ...
#
# Recommended deployment shape:
# CloudCLI is the only app using /ai, while root paths /api, /ws, and /shell
# are also proxied because the current frontend still calls those endpoints
# with root-relative URLs.
worker_processes 1;
events {
# Maximum simultaneous connections handled by each worker process.
# The default is enough for local testing and small self-hosted deployments.
worker_connections 1024;
}
http {
# WebSocket requests include an Upgrade header. Normal HTTP requests do not.
# This map gives us the right Connection header for both cases:
# Upgrade present -> "upgrade"
# Upgrade absent -> "close"
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
# For HTTPS deployments, replace this with `listen 443 ssl http2;` and
# add ssl_certificate / ssl_certificate_key lines.
listen 80 default_server;
# Use your real hostname in production, for example:
# server_name cloudcli.example.com;
server_name localhost 127.0.0.1;
# ---- User settings -------------------------------------------------
#
# Public path prefix where users access CloudCLI.
# Do not add a trailing slash.
#
# This variable can be used in redirects and response rewrites. It
# cannot be used in `location` matchers, so update the [SUBPATH LITERAL]
# lines too if you change it.
set $cloudcli_subpath /ai;
# Private upstream URL where the CloudCLI server is listening.
# For a default local server this is usually http://127.0.0.1:3001.
set $cloudcli_upstream http://127.0.0.1:3001;
# Allow larger file uploads through the code editor/project file APIs.
client_max_body_size 100m;
# Redirect /ai to /ai/ so relative browser URL resolution is stable.
# [SUBPATH LITERAL] Change `/ai` if you change $cloudcli_subpath.
location = /ai {
return 301 $cloudcli_subpath/;
}
# Main prefixed CloudCLI UI route.
#
# [SUBPATH LITERAL] Change `/ai/` and the `^/ai` rewrite if you change
# $cloudcli_subpath.
location ^~ /ai/ {
# Strip the public subpath before proxying. CloudCLI expects to see
# root paths such as /, /session/:id, /assets/..., /manifest.json.
rewrite ^/ai(?<cloudcli_path>/.*)$ $cloudcli_path break;
# Forward the rewritten request to the private CloudCLI server.
proxy_pass $cloudcli_upstream;
# Use HTTP/1.1 so WebSocket upgrade requests can pass through if a
# browser reaches a socket endpoint under the subpath.
proxy_http_version 1.1;
# Preserve useful request metadata for logs and future app support.
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix $cloudcli_subpath;
# WebSocket upgrade headers. Harmless for normal HTTP requests.
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Long-running agent and terminal sessions can stay open for a long
# time, so avoid closing idle proxied connections too aggressively.
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Disable gzip from the upstream response so sub_filter can inspect
# and rewrite HTML/JSON/JS response bodies.
proxy_set_header Accept-Encoding "";
# Rewrite browser-visible root-relative URLs so the runtime can
# discover that the app is mounted under the subpath.
#
# Examples:
# href="/manifest.json" -> href="/ai/manifest.json"
# src="/assets/app.js" -> src="/ai/assets/app.js"
#
# These rewrites are important for React Router basename detection.
sub_filter_once off;
sub_filter_types
application/json
application/manifest+json
application/javascript
text/javascript;
sub_filter 'href="/' 'href="$cloudcli_subpath/';
sub_filter 'src="/' 'src="$cloudcli_subpath/';
# The production HTML and JS register the service worker at /sw.js.
# Rewrite that registration so the worker is served from /ai/sw.js.
sub_filter "register('/sw.js')" "register('$cloudcli_subpath/sw.js')";
sub_filter 'register("/sw.js")' 'register("$cloudcli_subpath/sw.js")';
# The manifest and service worker contain root-relative paths too.
# Rewriting them keeps PWA metadata and cached manifest requests
# under the same public subpath.
sub_filter '"start_url": "/"' '"start_url": "$cloudcli_subpath/"';
sub_filter '"scope": "/"' '"scope": "$cloudcli_subpath/"';
sub_filter '"src": "/' '"src": "$cloudcli_subpath/';
sub_filter "'/manifest.json'" "'$cloudcli_subpath/manifest.json'";
sub_filter '"/manifest.json"' '"$cloudcli_subpath/manifest.json"';
}
# Root API proxy.
#
# The current CloudCLI frontend calls APIs with root-relative URLs such
# as /api/auth/login. Keep this location unless the frontend becomes
# fully prefix-aware for API requests.
location ^~ /api/ {
proxy_pass $cloudcli_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix $cloudcli_subpath;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# Main app WebSocket proxy.
#
# The frontend opens /ws for realtime chat/session/task updates.
location /ws {
proxy_pass $cloudcli_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix $cloudcli_subpath;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# Shell WebSocket proxy.
#
# The browser terminal uses /shell. It requires the same WebSocket
# upgrade handling as /ws.
location /shell {
proxy_pass $cloudcli_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix $cloudcli_subpath;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# Optional health endpoint proxy used by the frontend version checker.
location = /health {
proxy_pass $cloudcli_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix $cloudcli_subpath;
}
}
}

787
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@cloudcli-ai/cloudcli", "name": "@cloudcli-ai/cloudcli",
"version": "1.33.1", "version": "1.33.0",
"description": "A web-based UI for Claude Code CLI", "description": "A web-based UI for Claude Code CLI",
"type": "module", "type": "module",
"main": "dist-server/server/index.js", "main": "dist-server/server/index.js",

View File

@@ -11,8 +11,7 @@ export const CLAUDE_MODELS = {
{ {
value: "default", value: "default",
label: "Default (recommended)", label: "Default (recommended)",
description: description: "Use the default model (currently Opus 4.8 (1M context)) · $5/$25 per Mtok",
"Use the default model (currently Opus 4.8 (1M context)) · $5/$25 per Mtok",
}, },
{ {
value: "sonnet", value: "sonnet",
@@ -24,12 +23,6 @@ export const CLAUDE_MODELS = {
label: "Sonnet (1M context)", label: "Sonnet (1M context)",
description: "Sonnet 4.6 for long sessions · $3/$15 per Mtok", description: "Sonnet 4.6 for long sessions · $3/$15 per Mtok",
}, },
{
value: "opus[1m]",
label: "Opus 4.8 (1M context)",
description:
"Opus 4.8 with 1M context · Most capable for complex work · $5/$25 per Mtok",
},
{ {
value: "haiku", value: "haiku",
label: "Haiku", label: "Haiku",

View File

@@ -304,11 +304,7 @@ function extractTokenBudget(sdkMessage) {
const messageUsage = sdkMessage.message?.usage || sdkMessage.usage; const messageUsage = sdkMessage.message?.usage || sdkMessage.usage;
if (messageUsage && typeof messageUsage === 'object') { if (messageUsage && typeof messageUsage === 'object') {
const directInputTokens = readNumber(messageUsage.input_tokens ?? messageUsage.inputTokens); const inputTokens = readNumber(messageUsage.input_tokens ?? messageUsage.inputTokens);
const cacheCreationTokens = readNumber(messageUsage.cache_creation_input_tokens ?? messageUsage.cacheCreationInputTokens ?? messageUsage.cacheCreationTokens);
const cacheReadTokens = readNumber(messageUsage.cache_read_input_tokens ?? messageUsage.cacheReadInputTokens ?? messageUsage.cacheReadTokens);
const cacheTokens = cacheCreationTokens + cacheReadTokens;
const inputTokens = directInputTokens + cacheTokens;
const outputTokens = readNumber(messageUsage.output_tokens ?? messageUsage.outputTokens); const outputTokens = readNumber(messageUsage.output_tokens ?? messageUsage.outputTokens);
const totalUsed = inputTokens + outputTokens; const totalUsed = inputTokens + outputTokens;
const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000; const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
@@ -318,9 +314,6 @@ function extractTokenBudget(sdkMessage) {
total: contextWindow, total: contextWindow,
inputTokens, inputTokens,
outputTokens, outputTokens,
cacheReadTokens,
cacheCreationTokens,
cacheTokens,
breakdown: { breakdown: {
input: inputTokens, input: inputTokens,
output: outputTokens, output: outputTokens,

View File

@@ -87,11 +87,6 @@ const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
console.log('SERVER_PORT from env:', process.env.SERVER_PORT); console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
function readUsageNumber(value) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
}
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
@@ -1391,8 +1386,6 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000; const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
let inputTokens = 0; let inputTokens = 0;
let outputTokens = 0; let outputTokens = 0;
let cacheReadTokens = 0;
let cacheCreationTokens = 0;
// Find the latest assistant message with usage data (scan from end) // Find the latest assistant message with usage data (scan from end)
for (let i = lines.length - 1; i >= 0; i--) { for (let i = lines.length - 1; i >= 0; i--) {
@@ -1404,11 +1397,8 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
const usage = entry.message.usage; const usage = entry.message.usage;
// Use token counts from latest assistant message only // Use token counts from latest assistant message only
const directInputTokens = readUsageNumber(usage.input_tokens ?? usage.inputTokens); inputTokens = usage.input_tokens || 0;
cacheReadTokens = readUsageNumber(usage.cache_read_input_tokens ?? usage.cacheReadInputTokens ?? usage.cacheReadTokens); outputTokens = usage.output_tokens || 0;
cacheCreationTokens = readUsageNumber(usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens ?? usage.cacheCreationTokens);
inputTokens = directInputTokens + cacheReadTokens + cacheCreationTokens;
outputTokens = readUsageNumber(usage.output_tokens ?? usage.outputTokens);
break; // Stop after finding the latest assistant message break; // Stop after finding the latest assistant message
} }
@@ -1419,16 +1409,12 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
} }
const totalUsed = inputTokens + outputTokens; const totalUsed = inputTokens + outputTokens;
const cacheTokens = cacheReadTokens + cacheCreationTokens;
res.json({ res.json({
used: totalUsed, used: totalUsed,
total: contextWindow, total: contextWindow,
inputTokens, inputTokens,
outputTokens, outputTokens,
cacheReadTokens,
cacheCreationTokens,
cacheTokens,
breakdown: { breakdown: {
input: inputTokens, input: inputTokens,
output: outputTokens output: outputTokens
@@ -1497,133 +1483,74 @@ function permToRwx(perm) {
return r + w + x; return r + w + x;
} }
// Directories that are almost never interesting for a project tree but can
// contain tens of thousands of files. Skipping them before recursion keeps
// traversal time bounded on large monorepos and high-latency filesystems
// (NFS / SMB).
const IGNORED_DIRS = new Set([
// JS / TS toolchains
'node_modules', 'dist', 'build', '.next', '.nuxt', '.cache', '.parcel-cache',
// VCS
'.git', '.svn', '.hg',
// Python
'__pycache__', '.pytest_cache', '.mypy_cache', '.tox', 'venv', '.venv',
// Rust / Go / Java / Ruby
'target', 'vendor',
// Build output / IDE
'.gradle', '.idea', 'coverage', '.nyc_output'
]);
const DEFAULT_FS_CONCURRENCY = 64;
const parsedFsConcurrency = Number.parseInt(process.env.FS_CONCURRENCY || '', 10);
const FS_CONCURRENCY = Number.isFinite(parsedFsConcurrency) && parsedFsConcurrency > 0
? parsedFsConcurrency
: DEFAULT_FS_CONCURRENCY;
let activeFsOperations = 0;
const pendingFsOperations = [];
async function acquire() {
if (activeFsOperations < FS_CONCURRENCY) {
activeFsOperations += 1;
return;
}
await new Promise((resolve) => {
pendingFsOperations.push(resolve);
});
}
function release() {
const next = pendingFsOperations.shift();
if (next) {
next();
return;
}
activeFsOperations = Math.max(0, activeFsOperations - 1);
}
async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true) { async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true) {
// Using fsPromises from import // Using fsPromises from import
let entries; const items = [];
try { try {
await acquire(); const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
try {
entries = await fsPromises.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) {
} finally { // Debug: log all entries including hidden files
release();
// Skip heavy build directories and VCS directories
if (entry.name === 'node_modules' ||
entry.name === 'dist' ||
entry.name === 'build' ||
entry.name === '.git' ||
entry.name === '.svn' ||
entry.name === '.hg') continue;
const itemPath = path.join(dirPath, entry.name);
const item = {
name: entry.name,
path: itemPath,
type: entry.isDirectory() ? 'directory' : 'file'
};
// Get file stats for additional metadata
try {
const stats = await fsPromises.stat(itemPath);
item.size = stats.size;
item.modified = stats.mtime.toISOString();
// Convert permissions to rwx format
const mode = stats.mode;
const ownerPerm = (mode >> 6) & 7;
const groupPerm = (mode >> 3) & 7;
const otherPerm = mode & 7;
item.permissions = ((mode >> 6) & 7).toString() + ((mode >> 3) & 7).toString() + (mode & 7).toString();
item.permissionsRwx = permToRwx(ownerPerm) + permToRwx(groupPerm) + permToRwx(otherPerm);
} catch (statError) {
// If stat fails, provide default values
item.size = 0;
item.modified = null;
item.permissions = '000';
item.permissionsRwx = '---------';
}
if (entry.isDirectory() && currentDepth < maxDepth) {
// Recursively get subdirectories but limit depth
try {
// Check if we can access the directory before trying to read it
await fsPromises.access(item.path, fs.constants.R_OK);
item.children = await getFileTree(item.path, maxDepth, currentDepth + 1, showHidden);
} catch (e) {
// Silently skip directories we can't access (permission denied, etc.)
item.children = [];
}
}
items.push(item);
} }
} catch (error) { } catch (error) {
// Only log non-permission errors to avoid spam // Only log non-permission errors to avoid spam
if (error.code !== 'EACCES' && error.code !== 'EPERM') { if (error.code !== 'EACCES' && error.code !== 'EPERM') {
console.error('Error reading directory:', error); console.error('Error reading directory:', error);
} }
return [];
} }
const filteredEntries = entries.filter((entry) => !(entry.isDirectory() && IGNORED_DIRS.has(entry.name)));
// Process every entry in parallel. On high-latency filesystems (NFS/SMB)
// serial stat() was the real bottleneck — issuing them concurrently lets
// the kernel pipeline the round-trips and the recursive calls overlap too.
const items = await Promise.all(filteredEntries.map(async (entry) => {
const itemPath = path.join(dirPath, entry.name);
const item = {
name: entry.name,
path: itemPath,
type: entry.isDirectory() ? 'directory' : 'file'
};
// Get file stats for additional metadata
try {
await acquire();
try {
const stats = await fsPromises.lstat(itemPath);
item.size = stats.size;
item.modified = stats.mtime.toISOString();
// Mark symlinks so UI can distinguish them
if (stats.isSymbolicLink()) {
item.isSymlink = true;
}
// Convert permissions to rwx format
const mode = stats.mode;
const ownerPerm = (mode >> 6) & 7;
const groupPerm = (mode >> 3) & 7;
const otherPerm = mode & 7;
item.permissions =
((mode >> 6) & 7).toString() +
((mode >> 3) & 7).toString() +
(mode & 7).toString();
item.permissionsRwx =
permToRwx(ownerPerm) +
permToRwx(groupPerm) +
permToRwx(otherPerm);
} finally {
release();
}
} catch (statError) {
// If stat fails, provide default values
item.size = 0;
item.modified = null;
item.permissions = '000';
item.permissionsRwx = '---------';
}
if (entry.isDirectory() && currentDepth < maxDepth) {
// Recurse. Let readdir's own EACCES bubble up through the catch in
// the recursive call rather than doing a separate access() probe
// (which doubled the round-trip count on SMB without adding info).
// The recursive call starts with a bounded readdir; holding a permit
// for the whole subtree can deadlock when sibling directories are
// waiting on their own children.
item.children = await getFileTree(itemPath, maxDepth, currentDepth + 1, showHidden);
}
return item;
}));
return items.sort((a, b) => { return items.sort((a, b) => {
if (a.type !== b.type) { if (a.type !== b.type) {
return a.type === 'directory' ? -1 : 1; return a.type === 'directory' ? -1 : 1;

View File

@@ -18,23 +18,18 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
{ {
value: 'default', value: 'default',
label: 'Default (recommended)', label: 'Default (recommended)',
description: 'Use the default model (currently Opus 4.8 (1M context)) · $5/$25 per Mtok', description: 'Use the default model (currently Opus 4.7 (1M context)) · $5/$25 per Mtok',
}, },
{ {
value: "sonnet", value: 'sonnet',
label: "Sonnet", label: 'Sonnet',
description: "Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok", description: 'Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok',
}, },
{ {
value: 'sonnet[1m]', value: 'sonnet[1m]',
label: 'Sonnet (1M context)', label: 'Sonnet (1M context)',
description: 'Sonnet 4.6 for long sessions · $3/$15 per Mtok', description: 'Sonnet 4.6 for long sessions · $3/$15 per Mtok',
}, },
{
value: 'opus[1m]',
label: 'Opus 4.8 (1M context)',
description: 'Opus 4.8 with 1M context · Most capable for complex work · $5/$25 per Mtok',
},
{ {
value: 'haiku', value: 'haiku',
label: 'Haiku', label: 'Haiku',

View File

@@ -17,7 +17,6 @@ import { readProviderSessionActiveModelChange } from '@/shared/utils.js';
export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000; export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000;
const PROVIDER_MODELS_CACHE_VERSION = 1; const PROVIDER_MODELS_CACHE_VERSION = 1;
const UNCACHED_PROVIDERS = new Set<LLMProvider>(['claude']);
type ProviderModelsServiceDependencies = { type ProviderModelsServiceDependencies = {
resolveProvider?: (provider: LLMProvider) => Pick<IProvider, 'models'>; resolveProvider?: (provider: LLMProvider) => Pick<IProvider, 'models'>;
@@ -233,42 +232,10 @@ export const createProviderModelsService = (dependencies: ProviderModelsServiceD
return request; return request;
}; };
const loadDirectModels = (
provider: LLMProvider,
): Promise<ProviderModelsResult> => {
const request = resolveProvider(provider).models.getSupportedModels()
.then((models) => {
const currentTime = now();
return {
models,
cache: {
updatedAt: new Date(currentTime).toISOString(),
expiresAt: new Date(currentTime).toISOString(),
source: 'fresh' as const,
},
};
})
.finally(() => {
pendingRequests.delete(provider);
});
pendingRequests.set(provider, request);
return request;
};
const getProviderModels = async ( const getProviderModels = async (
provider: LLMProvider, provider: LLMProvider,
options: ProviderModelsOptions = {}, options: ProviderModelsOptions = {},
): Promise<ProviderModelsResult> => { ): Promise<ProviderModelsResult> => {
if (UNCACHED_PROVIDERS.has(provider)) {
const pendingRequest = pendingRequests.get(provider);
if (pendingRequest) {
return pendingRequest;
}
return loadDirectModels(provider);
}
if (options.bypassCache) { if (options.bypassCache) {
const pendingRequest = pendingRequests.get(provider); const pendingRequest = pendingRequests.get(provider);
if (pendingRequest) { if (pendingRequest) {

View File

@@ -130,37 +130,6 @@ test('provider models are cached for the three-day ttl', async () => {
} }
}); });
test('claude provider models are always loaded directly from the provider', async () => {
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-cache-claude-direct-'));
let loadCount = 0;
try {
const service = createProviderModelsService({
cachePath: path.join(tempRoot, 'models-cache.json'),
resolveProvider: (provider) => ({
models: {
getSupportedModels: async () => {
loadCount += 1;
return createModels(`${provider}-${loadCount}`);
},
getCurrentActiveModel: async () => createCurrentActiveModel(`${provider}-active`),
changeActiveModel: async (input) => createSessionActiveModelChange(provider, input),
},
}),
});
const first = await service.getProviderModels('claude');
const second = await service.getProviderModels('claude');
assert.equal(loadCount, 2);
assert.equal(first.models.DEFAULT, 'claude-1');
assert.equal(second.models.DEFAULT, 'claude-2');
assert.equal(second.cache.source, 'fresh');
} finally {
await rm(tempRoot, { recursive: true, force: true });
}
});
test('provider model cache is persisted across service instances', async () => { test('provider model cache is persisted across service instances', async () => {
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-cache-file-')); const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-cache-file-'));
const cachePath = path.join(tempRoot, 'models-cache.json'); const cachePath = path.join(tempRoot, 'models-cache.json');

View File

@@ -18,7 +18,6 @@ type ShellIncomingMessage = {
provider?: string; provider?: string;
initialCommand?: string; initialCommand?: string;
isPlainShell?: boolean; isPlainShell?: boolean;
forceRestart?: boolean;
}; };
type PtySessionEntry = { type PtySessionEntry = {
@@ -181,7 +180,6 @@ export function handleShellConnection(
const hasSession = readBoolean(data.hasSession); const hasSession = readBoolean(data.hasSession);
const provider = readString(data.provider, 'claude'); const provider = readString(data.provider, 'claude');
const initialCommand = readString(data.initialCommand); const initialCommand = readString(data.initialCommand);
const forceRestart = readBoolean(data.forceRestart);
const isPlainShell = const isPlainShell =
readBoolean(data.isPlainShell) || readBoolean(data.isPlainShell) ||
(!!initialCommand && !hasSession) || (!!initialCommand && !hasSession) ||
@@ -202,7 +200,7 @@ export function handleShellConnection(
: ''; : '';
ptySessionKey = `${projectPath}_${sessionId ?? 'default'}${commandSuffix}`; ptySessionKey = `${projectPath}_${sessionId ?? 'default'}${commandSuffix}`;
if (isLoginCommand || forceRestart) { if (isLoginCommand) {
const oldSession = ptySessionsMap.get(ptySessionKey); const oldSession = ptySessionsMap.get(ptySessionKey);
if (oldSession) { if (oldSession) {
if (oldSession.timeoutId) { if (oldSession.timeoutId) {
@@ -213,8 +211,7 @@ export function handleShellConnection(
} }
} }
const existingSession = const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
isLoginCommand || forceRestart ? null : ptySessionsMap.get(ptySessionKey);
if (existingSession) { if (existingSession) {
shellProcess = existingSession.pty; shellProcess = existingSession.pty;
if (existingSession.timeoutId) { if (existingSession.timeoutId) {
@@ -371,10 +368,6 @@ export function handleShellConnection(
} }
const session = ptySessionsMap.get(ptySessionKey); const session = ptySessionsMap.get(ptySessionKey);
if (session && session.pty !== shellProcess) {
return;
}
if (session && session.ws && session.ws.readyState === WebSocket.OPEN) { if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
session.ws.send( session.ws.send(
JSON.stringify({ JSON.stringify({
@@ -458,10 +451,6 @@ export function handleShellConnection(
session.ws = null; session.ws = null;
session.timeoutId = setTimeout(() => { session.timeoutId = setTimeout(() => {
if (ptySessionsMap.get(ptySessionKey as string) !== session) {
return;
}
session.pty.kill(); session.pty.kill();
ptySessionsMap.delete(ptySessionKey as string); ptySessionsMap.delete(ptySessionKey as string);
}, PTY_SESSION_TIMEOUT); }, PTY_SESSION_TIMEOUT);

View File

@@ -20,13 +20,7 @@ export function verifyWebSocketClient(
dependencies: WebSocketAuthDependencies dependencies: WebSocketAuthDependencies
): boolean { ): boolean {
const request = info.req as AuthenticatedWebSocketRequest; const request = info.req as AuthenticatedWebSocketRequest;
const upgradeUrl = new URL(request.url ?? '/', 'http://localhost'); console.log('WebSocket connection attempt to:', request.url);
const loggedUrl = new URL(upgradeUrl);
if (loggedUrl.searchParams.has('token')) {
loggedUrl.searchParams.set('token', 'REDACTED');
}
console.log('WebSocket connection attempt to:', `${loggedUrl.pathname}${loggedUrl.search}`);
// Platform mode: use the first DB user and skip token checks. // Platform mode: use the first DB user and skip token checks.
if (dependencies.isPlatform) { if (dependencies.isPlatform) {
@@ -42,6 +36,7 @@ export function verifyWebSocketClient(
} }
// OSS mode: read JWT from query string first, then Authorization header. // OSS mode: read JWT from query string first, then Authorization header.
const upgradeUrl = new URL(request.url ?? '/', 'http://localhost');
const token = const token =
upgradeUrl.searchParams.get('token') ?? upgradeUrl.searchParams.get('token') ??
request.headers.authorization?.split(' ')[1] ?? request.headers.authorization?.split(' ')[1] ??

View File

@@ -592,14 +592,12 @@ class ResponseCollector {
} }
} }
const inputTokens = totalInput + totalCacheRead + totalCacheCreation;
return { return {
inputTokens, inputTokens: totalInput,
outputTokens: totalOutput, outputTokens: totalOutput,
cacheReadTokens: totalCacheRead, cacheReadTokens: totalCacheRead,
cacheCreationTokens: totalCacheCreation, cacheCreationTokens: totalCacheCreation,
totalTokens: inputTokens + totalOutput totalTokens: totalInput + totalOutput + totalCacheRead + totalCacheCreation
}; };
} }
} }

View File

@@ -268,35 +268,16 @@ Custom commands can be created in:
tokenUsage.contextWindow ?? tokenUsage.contextWindow ??
0, 0,
) || 0; ) || 0;
const normalizedInputValue = const inputTokensRaw =
tokenUsage.inputTokens ??
tokenUsage.input ??
tokenUsage.cumulativeInputTokens ??
tokenUsage.breakdown?.input ??
tokenUsage.promptTokens;
const directInputTokens =
Number( Number(
normalizedInputValue ?? tokenUsage.inputTokens ??
tokenUsage.input ??
tokenUsage.input_tokens ?? tokenUsage.input_tokens ??
0 tokenUsage.cumulativeInputTokens ??
) || 0; tokenUsage.breakdown?.input ??
const cacheReadTokens = tokenUsage.promptTokens ??
Number(
tokenUsage.cacheReadTokens ??
tokenUsage.cache_read_input_tokens ??
tokenUsage.cacheReadInputTokens ??
0, 0,
) || 0; ) || 0;
const cacheCreationTokens =
Number(
tokenUsage.cacheCreationTokens ??
tokenUsage.cache_creation_input_tokens ??
tokenUsage.cacheCreationInputTokens ??
0,
) || 0;
const inputTokens = normalizedInputValue == null
? directInputTokens + cacheReadTokens + cacheCreationTokens
: directInputTokens;
const outputTokens = const outputTokens =
Number( Number(
tokenUsage.outputTokens ?? tokenUsage.outputTokens ??
@@ -307,9 +288,8 @@ Custom commands can be created in:
tokenUsage.completionTokens ?? tokenUsage.completionTokens ??
0, 0,
) || 0; ) || 0;
const computedUsed = inputTokens + outputTokens; const hasTokenBreakdown = inputTokensRaw > 0 || outputTokens > 0;
const hasTokenBreakdown = computedUsed > 0; const used = reportedUsed || inputTokensRaw + outputTokens;
const used = Math.max(reportedUsed, computedUsed);
return { return {
type: "builtin", type: "builtin",
@@ -322,7 +302,7 @@ Custom commands can be created in:
...(hasTokenBreakdown ...(hasTokenBreakdown
? { ? {
tokenBreakdown: { tokenBreakdown: {
input: inputTokens, input: inputTokensRaw,
output: outputTokens, output: outputTokens,
}, },
} }

View File

@@ -0,0 +1,44 @@
import { Brain, Zap, Sparkles, Atom } from 'lucide-react';
export const thinkingModes = [
{
id: 'none',
name: 'Standard',
description: 'Regular Claude response',
icon: null,
prefix: '',
color: 'text-gray-600'
},
{
id: 'think',
name: 'Think',
description: 'Basic extended thinking',
icon: Brain,
prefix: 'think',
color: 'text-blue-600'
},
{
id: 'think-hard',
name: 'Think Hard',
description: 'More thorough evaluation',
icon: Zap,
prefix: 'think hard',
color: 'text-purple-600'
},
{
id: 'think-harder',
name: 'Think Harder',
description: 'Deep analysis with alternatives',
icon: Sparkles,
prefix: 'think harder',
color: 'text-indigo-600'
},
{
id: 'ultrathink',
name: 'Ultrathink',
description: 'Maximum thinking budget',
icon: Atom,
prefix: 'ultrathink',
color: 'text-red-600'
}
];

View File

@@ -12,6 +12,7 @@ import type {
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { authenticatedFetch } from '../../../utils/api'; import { authenticatedFetch } from '../../../utils/api';
import { thinkingModes } from '../constants/thinkingModes';
import { grantClaudeToolPermission } from '../utils/chatPermissions'; import { grantClaudeToolPermission } from '../utils/chatPermissions';
import { safeLocalStorage } from '../utils/chatStorage'; import { safeLocalStorage } from '../utils/chatStorage';
import type { import type {
@@ -203,6 +204,7 @@ export function useChatComposerState({
const [uploadingImages, setUploadingImages] = useState<Map<string, number>>(new Map()); const [uploadingImages, setUploadingImages] = useState<Map<string, number>>(new Map());
const [imageErrors, setImageErrors] = useState<Map<string, string>>(new Map()); const [imageErrors, setImageErrors] = useState<Map<string, string>>(new Map());
const [isTextareaExpanded, setIsTextareaExpanded] = useState(false); const [isTextareaExpanded, setIsTextareaExpanded] = useState(false);
const [thinkingMode, setThinkingMode] = useState('none');
const [commandModalPayload, setCommandModalPayload] = useState<CommandModalPayload | null>(null); const [commandModalPayload, setCommandModalPayload] = useState<CommandModalPayload | null>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -311,7 +313,7 @@ export function useChatComposerState({
}, [addMessage]); }, [addMessage]);
const executeCommand = useCallback( const executeCommand = useCallback(
async (command: SlashCommand, rawInput?: string, options?: { preserveInput?: boolean }) => { async (command: SlashCommand, rawInput?: string) => {
if (!command || !selectedProject) { if (!command || !selectedProject) {
return; return;
} }
@@ -368,10 +370,8 @@ export function useChatComposerState({
const result = (await response.json()) as CommandExecutionResult; const result = (await response.json()) as CommandExecutionResult;
if (result.type === 'builtin') { if (result.type === 'builtin') {
handleBuiltInCommand(result); handleBuiltInCommand(result);
if (!options?.preserveInput) { setInput('');
setInput(''); inputValueRef.current = '';
inputValueRef.current = '';
}
} else if (result.type === 'custom') { } else if (result.type === 'custom') {
await handleCustomCommand(result); await handleCustomCommand(result);
} }
@@ -402,19 +402,6 @@ export function useChatComposerState({
], ],
); );
const showCostModal = useCallback(() => {
executeCommand(
{
name: '/cost',
description: 'Display token usage information',
namespace: 'builtin',
metadata: { type: 'builtin' },
} as SlashCommand,
'/cost',
{ preserveInput: true },
);
}, [executeCommand]);
const { const {
slashCommands, slashCommands,
slashCommandsCount, slashCommandsCount,
@@ -575,7 +562,11 @@ export function useChatComposerState({
} }
} }
const messageContent = currentInput; let messageContent = currentInput;
const selectedThinkingMode = thinkingModes.find((mode: { id: string; prefix?: string }) => mode.id === thinkingMode);
if (selectedThinkingMode && selectedThinkingMode.prefix) {
messageContent = `${selectedThinkingMode.prefix}: ${currentInput}`;
}
let uploadedImages: unknown[] = []; let uploadedImages: unknown[] = [];
if (attachedImages.length > 0) { if (attachedImages.length > 0) {
@@ -758,6 +749,7 @@ export function useChatComposerState({
setUploadingImages(new Map()); setUploadingImages(new Map());
setImageErrors(new Map()); setImageErrors(new Map());
setIsTextareaExpanded(false); setIsTextareaExpanded(false);
setThinkingMode('none');
if (textareaRef.current) { if (textareaRef.current) {
textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = 'auto';
@@ -791,6 +783,7 @@ export function useChatComposerState({
setIsLoading, setIsLoading,
setIsUserScrolledUp, setIsUserScrolledUp,
slashCommands, slashCommands,
thinkingMode,
], ],
); );
@@ -1027,6 +1020,8 @@ export function useChatComposerState({
textareaRef, textareaRef,
inputHighlightRef, inputHighlightRef,
isTextareaExpanded, isTextareaExpanded,
thinkingMode,
setThinkingMode,
slashCommandsCount, slashCommandsCount,
filteredCommands, filteredCommands,
frequentCommands, frequentCommands,
@@ -1064,6 +1059,5 @@ export function useChatComposerState({
isInputFocused, isInputFocused,
commandModalPayload, commandModalPayload,
closeCommandModal, closeCommandModal,
showCostModal,
}; };
} }

View File

@@ -7,12 +7,6 @@ import type { NormalizedMessage } from '../../../stores/useSessionStore';
import type { ChatMessage, SubagentChildTool } from '../types/types'; import type { ChatMessage, SubagentChildTool } from '../types/types';
import { decodeHtmlEntities, unescapeWithMathProtection, formatUsageLimitText } from '../utils/chatFormatting'; import { decodeHtmlEntities, unescapeWithMathProtection, formatUsageLimitText } from '../utils/chatFormatting';
function formatToolResultContent(content: unknown): string {
const text = typeof content === 'string' ? content : JSON.stringify(content);
const toolUseErrorMatch = /^<tool_use_error>([\s\S]*)<\/tool_use_error>$/.exec(text.trim());
return toolUseErrorMatch ? toolUseErrorMatch[1] : text;
}
/** /**
* Convert NormalizedMessage[] from the session store into ChatMessage[] * Convert NormalizedMessage[] from the session store into ChatMessage[]
* that the existing UI components expect. * that the existing UI components expect.
@@ -26,12 +20,7 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
// First pass: collect tool results for attachment // First pass: collect tool results for attachment
const toolResultMap = new Map<string, NormalizedMessage>(); const toolResultMap = new Map<string, NormalizedMessage>();
const toolUseIds = new Set<string>();
for (const msg of messages) { for (const msg of messages) {
if (msg.kind === 'tool_use' && msg.toolId) {
toolUseIds.add(msg.toolId);
}
if (msg.kind === 'tool_result' && msg.toolId) { if (msg.kind === 'tool_result' && msg.toolId) {
toolResultMap.set(msg.toolId, msg); toolResultMap.set(msg.toolId, msg);
} }
@@ -108,7 +97,7 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
const toolResult = tr const toolResult = tr
? { ? {
content: formatToolResultContent(tr.content), content: typeof tr.content === 'string' ? tr.content : JSON.stringify(tr.content),
isError: Boolean(tr.isError), isError: Boolean(tr.isError),
toolUseResult: (tr as any).toolUseResult, toolUseResult: (tr as any).toolUseResult,
} }
@@ -202,25 +191,8 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
break; break;
// tool_result is handled via attachment to tool_use above // tool_result is handled via attachment to tool_use above
case 'tool_result': { case 'tool_result':
if (msg.toolId && toolUseIds.has(msg.toolId)) {
break;
}
const content = formatToolResultContent(msg.content || '');
if (!content.trim()) {
break;
}
converted.push({
type: msg.isError ? 'error' : 'assistant',
content,
timestamp: msg.timestamp,
toolId: msg.toolId,
...sharedMetadata,
});
break; break;
}
default: default:
break; break;

View File

@@ -564,15 +564,11 @@ export function shouldHideToolResult(toolName: string, toolResult: any): boolean
if (!config.result) return false; if (!config.result) return false;
// Hidden/success-only configs suppress noisy successful output, but errors
// still need to be visible so failed tool calls are diagnosable.
if (toolResult?.isError) return false;
// Always hidden // Always hidden
if (config.result.hidden) return true; if (config.result.hidden) return true;
// Hide on success only // Hide on success only
if (config.result.hideOnSuccess && toolResult) { if (config.result.hideOnSuccess && toolResult && !toolResult.isError) {
return true; return true;
} }

View File

@@ -141,6 +141,8 @@ function ChatInterface({
textareaRef, textareaRef,
inputHighlightRef, inputHighlightRef,
isTextareaExpanded, isTextareaExpanded,
thinkingMode,
setThinkingMode,
slashCommandsCount, slashCommandsCount,
filteredCommands, filteredCommands,
frequentCommands, frequentCommands,
@@ -178,7 +180,6 @@ function ChatInterface({
isInputFocused: _isInputFocused, isInputFocused: _isInputFocused,
commandModalPayload, commandModalPayload,
closeCommandModal, closeCommandModal,
showCostModal,
} = useChatComposerState({ } = useChatComposerState({
selectedProject, selectedProject,
selectedSession, selectedSession,
@@ -368,8 +369,9 @@ function ChatInterface({
provider={provider} provider={provider}
permissionMode={permissionMode} permissionMode={permissionMode}
onModeSwitch={cyclePermissionMode} onModeSwitch={cyclePermissionMode}
thinkingMode={thinkingMode}
setThinkingMode={setThinkingMode}
tokenBudget={tokenBudget} tokenBudget={tokenBudget}
onShowTokenUsage={showCostModal}
slashCommandsCount={slashCommandsCount} slashCommandsCount={slashCommandsCount}
onToggleCommandMenu={handleToggleCommandMenu} onToggleCommandMenu={handleToggleCommandMenu}
hasInput={Boolean(input.trim())} hasInput={Boolean(input.trim())}

View File

@@ -2,16 +2,23 @@ import { useTranslation } from 'react-i18next';
import type { import type {
ChangeEvent, ChangeEvent,
ClipboardEvent, ClipboardEvent,
Dispatch,
FormEvent, FormEvent,
KeyboardEvent, KeyboardEvent,
MouseEvent, MouseEvent,
ReactNode, ReactNode,
RefObject, RefObject,
SetStateAction,
TouchEvent, TouchEvent,
} from 'react'; } from 'react';
import { ImageIcon, MessageSquareIcon, XIcon, ArrowDownIcon } from 'lucide-react'; import { ImageIcon, MessageSquareIcon, XIcon, ArrowDownIcon } from 'lucide-react';
import type { PendingPermissionRequest, PermissionMode, Provider } from '../../types/types'; import type { PendingPermissionRequest, PermissionMode, Provider } from '../../types/types';
import CommandMenu from './CommandMenu';
import ClaudeStatus from './ClaudeStatus';
import ImageAttachment from './ImageAttachment';
import PermissionRequestsBanner from './PermissionRequestsBanner';
import ThinkingModeSelector from './ThinkingModeSelector';
import TokenUsageSummary from './TokenUsageSummary';
import { import {
PromptInput, PromptInput,
PromptInputHeader, PromptInputHeader,
@@ -23,12 +30,6 @@ import {
PromptInputSubmit, PromptInputSubmit,
} from '../../../../shared/view/ui'; } from '../../../../shared/view/ui';
import CommandMenu from './CommandMenu';
import ClaudeStatus from './ClaudeStatus';
import ImageAttachment from './ImageAttachment';
import PermissionRequestsBanner from './PermissionRequestsBanner';
import TokenUsageSummary from './TokenUsageSummary';
interface MentionableFile { interface MentionableFile {
name: string; name: string;
path: string; path: string;
@@ -57,8 +58,9 @@ interface ChatComposerProps {
provider: Provider | string; provider: Provider | string;
permissionMode: PermissionMode | string; permissionMode: PermissionMode | string;
onModeSwitch: () => void; onModeSwitch: () => void;
thinkingMode: string;
setThinkingMode: Dispatch<SetStateAction<string>>;
tokenBudget: Record<string, unknown> | null; tokenBudget: Record<string, unknown> | null;
onShowTokenUsage: () => void;
slashCommandsCount: number; slashCommandsCount: number;
onToggleCommandMenu: () => void; onToggleCommandMenu: () => void;
hasInput: boolean; hasInput: boolean;
@@ -111,8 +113,9 @@ export default function ChatComposer({
provider, provider,
permissionMode, permissionMode,
onModeSwitch, onModeSwitch,
thinkingMode,
setThinkingMode,
tokenBudget, tokenBudget,
onShowTokenUsage,
slashCommandsCount, slashCommandsCount,
onToggleCommandMenu, onToggleCommandMenu,
hasInput, hasInput,
@@ -355,7 +358,11 @@ export default function ChatComposer({
</div> </div>
</button> </button>
<TokenUsageSummary usage={tokenBudget} onClick={onShowTokenUsage} /> {provider === 'claude' && (
<ThinkingModeSelector selectedMode={thinkingMode} onModeChange={setThinkingMode} onClose={() => {}} className="" />
)}
<TokenUsageSummary usage={tokenBudget} />
<PromptInputButton <PromptInputButton
tooltip={{ content: t('input.showAllCommands') }} tooltip={{ content: t('input.showAllCommands') }}
@@ -376,7 +383,7 @@ export default function ChatComposer({
<PromptInputButton <PromptInputButton
tooltip={{ content: t('input.clearInput', { defaultValue: 'Clear input' }) }} tooltip={{ content: t('input.clearInput', { defaultValue: 'Clear input' }) }}
onClick={onClearInput} onClick={onClearInput}
className="hidden sm:flex" className="hidden sm:No-flex"
> >
<XIcon /> <XIcon />
</PromptInputButton> </PromptInputButton>
@@ -393,8 +400,7 @@ export default function ChatComposer({
{sendByCtrlEnter ? t('input.hintText.ctrlEnter') : t('input.hintText.enter')} {sendByCtrlEnter ? t('input.hintText.ctrlEnter') : t('input.hintText.enter')}
</div> </div>
<PromptInputSubmit <PromptInputSubmit
onClick={isLoading ? onAbortSession : undefined} disabled={!input.trim() || isLoading}
disabled={!isLoading && !input.trim()}
className="h-10 w-10 sm:h-10 sm:w-10" className="h-10 w-10 sm:h-10 sm:w-10"
/> />
</div> </div>

View File

@@ -1,6 +1,5 @@
import { memo, useEffect, useMemo, useRef, useState } from 'react'; import { memo, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo'; import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
import type { import type {
ChatMessage, ChatMessage,
@@ -9,10 +8,10 @@ import type {
Provider, Provider,
} from '../../types/types'; } from '../../types/types';
import { formatUsageLimitText } from '../../utils/chatFormatting'; import { formatUsageLimitText } from '../../utils/chatFormatting';
import { getClaudePermissionSuggestion } from '../../utils/chatPermissions';
import type { Project } from '../../../../types/app'; import type { Project } from '../../../../types/app';
import { ToolRenderer, shouldHideToolResult } from '../../tools'; import { ToolRenderer, shouldHideToolResult } from '../../tools';
import { Reasoning, ReasoningTrigger, ReasoningContent } from '../../../../shared/view/ui'; import { Reasoning, ReasoningTrigger, ReasoningContent } from '../../../../shared/view/ui';
import { Markdown } from './Markdown'; import { Markdown } from './Markdown';
import MessageCopyControl from './MessageCopyControl'; import MessageCopyControl from './MessageCopyControl';
@@ -42,9 +41,10 @@ type InteractiveOption = {
isSelected: boolean; isSelected: boolean;
}; };
type PermissionGrantState = 'idle' | 'granted' | 'error';
const COPY_HIDDEN_TOOL_NAMES = new Set(['Bash', 'Edit', 'Write', 'ApplyPatch']); const COPY_HIDDEN_TOOL_NAMES = new Set(['Bash', 'Edit', 'Write', 'ApplyPatch']);
const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, autoExpandTools, showRawParameters, showThinking, selectedProject, provider }: MessageComponentProps) => { const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, onShowSettings, onGrantToolPermission, autoExpandTools, showRawParameters, showThinking, selectedProject, provider }: MessageComponentProps) => {
const { t } = useTranslation('chat'); const { t } = useTranslation('chat');
const isGrouped = prevMessage && prevMessage.type === message.type && const isGrouped = prevMessage && prevMessage.type === message.type &&
((prevMessage.type === 'assistant') || ((prevMessage.type === 'assistant') ||
@@ -53,6 +53,8 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, a
(prevMessage.type === 'error')); (prevMessage.type === 'error'));
const messageRef = useRef<HTMLDivElement | null>(null); const messageRef = useRef<HTMLDivElement | null>(null);
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const permissionSuggestion = getClaudePermissionSuggestion(message, provider);
const [permissionGrantState, setPermissionGrantState] = useState<PermissionGrantState>('idle');
const userCopyContent = String(message.content || ''); const userCopyContent = String(message.content || '');
const formattedMessageContent = useMemo( const formattedMessageContent = useMemo(
() => formatUsageLimitText(String(message.content || '')), () => formatUsageLimitText(String(message.content || '')),
@@ -71,6 +73,10 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, a
!message.isThinking; !message.isThinking;
useEffect(() => {
setPermissionGrantState('idle');
}, [permissionSuggestion?.entry, message.toolId]);
useEffect(() => { useEffect(() => {
const node = messageRef.current; const node = messageRef.current;
if (!autoExpandTools || !node || !message.isToolUse) return; if (!autoExpandTools || !node || !message.isToolUse) return;
@@ -235,6 +241,55 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, a
<Markdown className="prose prose-sm prose-red max-w-none dark:prose-invert"> <Markdown className="prose prose-sm prose-red max-w-none dark:prose-invert">
{String(message.toolResult.content || '')} {String(message.toolResult.content || '')}
</Markdown> </Markdown>
{permissionSuggestion && (
<div className="mt-4 border-t border-red-200/60 pt-3 dark:border-red-800/60">
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
onClick={() => {
if (!onGrantToolPermission) return;
const result = onGrantToolPermission(permissionSuggestion);
if (result?.success) {
setPermissionGrantState('granted');
} else {
setPermissionGrantState('error');
}
}}
disabled={permissionSuggestion.isAllowed || permissionGrantState === 'granted'}
className={`inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors ${permissionSuggestion.isAllowed || permissionGrantState === 'granted'
? 'cursor-default border-green-300/70 bg-green-100 text-green-800 dark:border-green-800/60 dark:bg-green-900/30 dark:text-green-200'
: 'border-red-300/70 bg-white/80 text-red-700 hover:bg-white dark:border-red-800/60 dark:bg-gray-900/40 dark:text-red-200 dark:hover:bg-gray-900/70'
}`}
>
{permissionSuggestion.isAllowed || permissionGrantState === 'granted'
? t('permissions.added')
: t('permissions.grant', { tool: permissionSuggestion.toolName })}
</button>
{onShowSettings && (
<button
type="button"
onClick={(e) => { e.stopPropagation(); onShowSettings(); }}
className="text-xs text-red-700 underline hover:text-red-800 dark:text-red-200 dark:hover:text-red-100"
>
{t('permissions.openSettings')}
</button>
)}
</div>
<div className="mt-2 text-xs text-red-700/90 dark:text-red-200/80">
{t('permissions.addTo', { entry: permissionSuggestion.entry })}
</div>
{permissionGrantState === 'error' && (
<div className="mt-2 text-xs text-red-700 dark:text-red-200">
{t('permissions.error')}
</div>
)}
{(permissionSuggestion.isAllowed || permissionGrantState === 'granted') && (
<div className="mt-2 text-xs text-green-700 dark:text-green-200">
{t('permissions.retry')}
</div>
)}
</div>
)}
</div> </div>
</div> </div>
) : ( ) : (

View File

@@ -277,15 +277,11 @@ export default function ProviderSelectionEmptyState({
> >
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="truncate">{model.label}</div> <div className="truncate">{model.label}</div>
{/* {model.description && (
// * Temporarly commented out because the description of models from claude
// * was a bit inconsistent. Will return it back when it becomes more consistent.
*/}
{/* {model.description && (
<div className="truncate text-xs text-muted-foreground"> <div className="truncate text-xs text-muted-foreground">
{model.description} {model.description}
</div> </div>
)} */} )}
</div> </div>
{isSelected && ( {isSelected && (
<Check className="ml-auto h-4 w-4 shrink-0 text-primary" /> <Check className="ml-auto h-4 w-4 shrink-0 text-primary" />

View File

@@ -0,0 +1,244 @@
import { useState, useRef, useEffect, useCallback, type CSSProperties } from 'react';
import { createPortal } from 'react-dom';
import { Brain, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { thinkingModes } from '../../constants/thinkingModes';
type ThinkingModeSelectorProps = {
selectedMode: string;
onModeChange: (modeId: string) => void;
onClose?: () => void;
className?: string;
};
function ThinkingModeSelector({ selectedMode, onModeChange, onClose, className = '' }: ThinkingModeSelectorProps) {
const { t } = useTranslation('chat');
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const [dropdownStyle, setDropdownStyle] = useState<CSSProperties | null>(null);
// Mapping from mode ID to translation key
const modeKeyMap: Record<string, string> = {
'think-hard': 'thinkHard',
'think-harder': 'thinkHarder'
};
// Create translated modes for display
const translatedModes = thinkingModes.map(mode => {
const modeKey = modeKeyMap[mode.id] || mode.id;
return {
...mode,
name: t(`thinkingMode.modes.${modeKey}.name`),
description: t(`thinkingMode.modes.${modeKey}.description`),
prefix: t(`thinkingMode.modes.${modeKey}.prefix`)
};
});
const closeDropdown = useCallback(() => {
setIsOpen(false);
onClose?.();
}, [onClose]);
const updateDropdownPosition = useCallback(() => {
const trigger = triggerRef.current;
const dropdown = dropdownRef.current;
if (!trigger || !dropdown || typeof window === 'undefined') {
return;
}
const triggerRect = trigger.getBoundingClientRect();
const viewportPadding = window.innerWidth < 640 ? 12 : 16;
const spacing = 8;
const width = Math.min(window.innerWidth - viewportPadding * 2, window.innerWidth < 640 ? 320 : 256);
let left = triggerRect.left + triggerRect.width / 2 - width / 2;
left = Math.max(viewportPadding, Math.min(left, window.innerWidth - width - viewportPadding));
const measuredHeight = dropdown.offsetHeight || 0;
const spaceBelow = window.innerHeight - triggerRect.bottom - spacing - viewportPadding;
const spaceAbove = triggerRect.top - spacing - viewportPadding;
const openBelow = spaceBelow >= Math.min(measuredHeight || 320, 320) || spaceBelow >= spaceAbove;
const availableHeight = Math.min(
window.innerHeight - viewportPadding * 2,
Math.max(180, openBelow ? spaceBelow : spaceAbove),
);
const panelHeight = Math.min(measuredHeight || availableHeight, availableHeight);
const top = openBelow
? Math.min(triggerRect.bottom + spacing, window.innerHeight - viewportPadding - panelHeight)
: Math.max(viewportPadding, triggerRect.top - spacing - panelHeight);
setDropdownStyle({
position: 'fixed',
top,
left,
width,
maxHeight: availableHeight,
zIndex: 80,
});
}, []);
useEffect(() => {
if (!isOpen) {
setDropdownStyle(null);
return;
}
const rafId = window.requestAnimationFrame(updateDropdownPosition);
const handleViewportChange = () => updateDropdownPosition();
window.addEventListener('resize', handleViewportChange);
window.addEventListener('scroll', handleViewportChange, true);
return () => {
window.cancelAnimationFrame(rafId);
window.removeEventListener('resize', handleViewportChange);
window.removeEventListener('scroll', handleViewportChange, true);
};
}, [isOpen, updateDropdownPosition]);
useEffect(() => {
if (!isOpen) {
return;
}
const handlePointerDown = (event: PointerEvent) => {
const target = event.target;
if (!(target instanceof Node)) {
return;
}
if (containerRef.current?.contains(target) || dropdownRef.current?.contains(target)) {
return;
}
closeDropdown();
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeDropdown();
}
};
document.addEventListener('pointerdown', handlePointerDown, true);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('pointerdown', handlePointerDown, true);
document.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, closeDropdown]);
const currentMode = translatedModes.find(mode => mode.id === selectedMode) || translatedModes[0];
const IconComponent = currentMode.icon || Brain;
return (
<div className={`relative ${className}`} ref={containerRef}>
<button
ref={triggerRef}
type="button"
onClick={() => {
if (isOpen) {
closeDropdown();
return;
}
setIsOpen(true);
}}
className={`flex h-10 w-10 items-center justify-center rounded-full transition-all duration-200 sm:h-10 sm:w-10 ${selectedMode === 'none'
? 'bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600'
: 'bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800'
}`}
title={t('thinkingMode.buttonTitle', { mode: currentMode.name })}
aria-haspopup="dialog"
aria-expanded={isOpen}
>
<IconComponent className={`h-5 w-5 ${currentMode.color}`} />
</button>
{isOpen && typeof document !== 'undefined' && createPortal(
<div
ref={dropdownRef}
style={dropdownStyle || { position: 'fixed', top: 0, left: 0, visibility: 'hidden' }}
className="flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800"
role="dialog"
aria-modal="false"
>
<div className="border-b border-gray-200 p-3 dark:border-gray-700">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
{t('thinkingMode.selector.title')}
</h3>
<button
type="button"
onClick={closeDropdown}
className="rounded p-1 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<X className="h-4 w-4 text-gray-500" />
</button>
</div>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t('thinkingMode.selector.description')}
</p>
</div>
<div className="min-h-0 overflow-y-auto py-1">
{translatedModes.map((mode) => {
const ModeIcon = mode.icon;
const isSelected = mode.id === selectedMode;
return (
<button
key={mode.id}
type="button"
onClick={() => {
onModeChange(mode.id);
closeDropdown();
}}
className={`w-full px-4 py-3 text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-700 ${isSelected ? 'bg-gray-50 dark:bg-gray-700' : ''
}`}
>
<div className="flex items-start gap-3">
<div className={`mt-0.5 ${mode.icon ? mode.color : 'text-gray-400'}`}>
{ModeIcon ? <ModeIcon className="h-5 w-5" /> : <div className="h-5 w-5" />}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className={`text-sm font-medium ${isSelected ? 'text-gray-900 dark:text-white' : 'text-gray-700 dark:text-gray-300'
}`}>
{mode.name}
</span>
{isSelected && (
<span className="rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-700 dark:bg-blue-900 dark:text-blue-300">
{t('thinkingMode.selector.active')}
</span>
)}
</div>
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{mode.description}
</p>
{mode.prefix && (
<code className="mt-1 inline-block rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-700">
{mode.prefix}
</code>
)}
</div>
</div>
</button>
);
})}
</div>
<div className="border-t border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900">
<p className="text-xs text-gray-600 dark:text-gray-400">
<strong>Tip:</strong> {t('thinkingMode.selector.tip')}
</p>
</div>
</div>,
document.body
)}
</div>
);
}
export default ThinkingModeSelector;

View File

@@ -2,7 +2,6 @@ import { ActivityIcon } from 'lucide-react';
type TokenUsageSummaryProps = { type TokenUsageSummaryProps = {
usage: Record<string, unknown> | null; usage: Record<string, unknown> | null;
onClick?: () => void;
}; };
const formatTokenCount = (value: number) => { const formatTokenCount = (value: number) => {
@@ -30,7 +29,7 @@ const readUsageNumber = (value: unknown) => {
return Number.isFinite(parsed) ? parsed : 0; return Number.isFinite(parsed) ? parsed : 0;
}; };
export default function TokenUsageSummary({ usage, onClick }: TokenUsageSummaryProps) { export default function TokenUsageSummary({ usage }: TokenUsageSummaryProps) {
const breakdown = const breakdown =
usage?.breakdown && typeof usage.breakdown === 'object' usage?.breakdown && typeof usage.breakdown === 'object'
? usage.breakdown as Record<string, unknown> ? usage.breakdown as Record<string, unknown>
@@ -40,18 +39,15 @@ export default function TokenUsageSummary({ usage, onClick }: TokenUsageSummaryP
const usedTokens = readUsageNumber(usage?.used) || inputTokens + outputTokens; const usedTokens = readUsageNumber(usage?.used) || inputTokens + outputTokens;
return ( return (
<button <div
type="button" className="inline-flex h-9 items-center gap-1.5 rounded-lg border border-border/70 bg-background/70 px-2 text-xs text-muted-foreground shadow-sm transition-colors hover:border-primary/25 hover:text-foreground sm:gap-2 sm:px-2.5"
onClick={onClick}
className="inline-flex h-9 items-center gap-1.5 rounded-lg border border-border/70 bg-background/70 px-2 text-xs text-muted-foreground shadow-sm transition-colors hover:border-primary/25 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 sm:gap-2 sm:px-2.5"
title={`${usedTokens.toLocaleString()} tokens used`} title={`${usedTokens.toLocaleString()} tokens used`}
aria-label="Show token usage"
> >
<span className="grid h-5 w-5 place-items-center rounded-md bg-primary/10 text-primary"> <span className="grid h-5 w-5 place-items-center rounded-md bg-primary/10 text-primary">
<ActivityIcon className="h-3.5 w-3.5" /> <ActivityIcon className="h-3.5 w-3.5" />
</span> </span>
<span className="font-medium text-foreground">{formatTokenCount(usedTokens)}</span> <span className="font-medium text-foreground">{formatTokenCount(usedTokens)}</span>
<span className="hidden text-muted-foreground/70 sm:inline">tokens</span> <span className="hidden text-muted-foreground/70 sm:inline">tokens</span>
</button> </div>
); );
} }

View File

@@ -2,7 +2,6 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import type { MutableRefObject } from 'react'; import type { MutableRefObject } from 'react';
import type { FitAddon } from '@xterm/addon-fit'; import type { FitAddon } from '@xterm/addon-fit';
import type { Terminal } from '@xterm/xterm'; import type { Terminal } from '@xterm/xterm';
import type { Project, ProjectSession } from '../../../types/app'; import type { Project, ProjectSession } from '../../../types/app';
import { TERMINAL_INIT_DELAY_MS } from '../constants/constants'; import { TERMINAL_INIT_DELAY_MS } from '../constants/constants';
import { getShellWebSocketUrl, parseShellMessage, sendSocketMessage } from '../utils/socket'; import { getShellWebSocketUrl, parseShellMessage, sendSocketMessage } from '../utils/socket';
@@ -32,8 +31,8 @@ type UseShellConnectionResult = {
isConnected: boolean; isConnected: boolean;
isConnecting: boolean; isConnecting: boolean;
closeSocket: () => void; closeSocket: () => void;
connectToShell: (options?: { forceRestart?: boolean }) => void; connectToShell: () => void;
disconnectFromShell: (options?: { suppressAutoConnect?: boolean }) => void; disconnectFromShell: () => void;
}; };
export function useShellConnection({ export function useShellConnection({
@@ -55,8 +54,6 @@ export function useShellConnection({
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false); const [isConnecting, setIsConnecting] = useState(false);
const connectingRef = useRef(false); const connectingRef = useRef(false);
const forceRestartOnInitRef = useRef(false);
const suppressAutoConnectRef = useRef(false);
const handleProcessCompletion = useCallback( const handleProcessCompletion = useCallback(
(output: string) => { (output: string) => {
@@ -144,8 +141,6 @@ export function useShellConnection({
} }
currentFitAddon.fit(); currentFitAddon.fit();
const forceRestart = forceRestartOnInitRef.current;
forceRestartOnInitRef.current = false;
sendSocketMessage(socket, { sendSocketMessage(socket, {
type: 'init', type: 'init',
@@ -157,7 +152,6 @@ export function useShellConnection({
rows: currentTerminal.rows, rows: currentTerminal.rows,
initialCommand: initialCommandRef.current, initialCommand: initialCommandRef.current,
isPlainShell: isPlainShellRef.current, isPlainShell: isPlainShellRef.current,
forceRestart,
}); });
}, TERMINAL_INIT_DELAY_MS); }, TERMINAL_INIT_DELAY_MS);
}; };
@@ -183,7 +177,6 @@ export function useShellConnection({
setIsConnected(false); setIsConnected(false);
setIsConnecting(false); setIsConnecting(false);
connectingRef.current = false; connectingRef.current = false;
forceRestartOnInitRef.current = false;
} }
}, },
[ [
@@ -202,40 +195,27 @@ export function useShellConnection({
], ],
); );
const connectToShell = useCallback((options?: { forceRestart?: boolean }) => { const connectToShell = useCallback(() => {
if (!isInitialized || isConnected || isConnecting || connectingRef.current) { if (!isInitialized || isConnected || isConnecting || connectingRef.current) {
return; return;
} }
forceRestartOnInitRef.current = Boolean(options?.forceRestart);
suppressAutoConnectRef.current = false;
connectingRef.current = true; connectingRef.current = true;
setIsConnecting(true); setIsConnecting(true);
connectWebSocket(true); connectWebSocket(true);
}, [connectWebSocket, isConnected, isConnecting, isInitialized]); }, [connectWebSocket, isConnected, isConnecting, isInitialized]);
const disconnectFromShell = useCallback((options?: { suppressAutoConnect?: boolean }) => { const disconnectFromShell = useCallback(() => {
if (options?.suppressAutoConnect) {
suppressAutoConnectRef.current = true;
}
closeSocket(); closeSocket();
clearTerminalScreen(); clearTerminalScreen();
setIsConnected(false); setIsConnected(false);
setIsConnecting(false); setIsConnecting(false);
connectingRef.current = false; connectingRef.current = false;
forceRestartOnInitRef.current = false;
setAuthUrl(''); setAuthUrl('');
}, [clearTerminalScreen, closeSocket, setAuthUrl]); }, [clearTerminalScreen, closeSocket, setAuthUrl]);
useEffect(() => { useEffect(() => {
if ( if (!autoConnect || !isInitialized || isConnecting || isConnected) {
!autoConnect ||
suppressAutoConnectRef.current ||
!isInitialized ||
isConnecting ||
isConnected
) {
return; return;
} }

View File

@@ -1,7 +1,6 @@
import type { MutableRefObject, RefObject } from 'react'; import type { MutableRefObject, RefObject } from 'react';
import type { FitAddon } from '@xterm/addon-fit'; import type { FitAddon } from '@xterm/addon-fit';
import type { Terminal } from '@xterm/xterm'; import type { Terminal } from '@xterm/xterm';
import type { Project, ProjectSession } from '../../../types/app'; import type { Project, ProjectSession } from '../../../types/app';
export type AuthCopyStatus = 'idle' | 'copied' | 'failed'; export type AuthCopyStatus = 'idle' | 'copied' | 'failed';
@@ -16,7 +15,6 @@ export type ShellInitMessage = {
rows: number; rows: number;
initialCommand: string | null | undefined; initialCommand: string | null | undefined;
isPlainShell: boolean; isPlainShell: boolean;
forceRestart?: boolean;
}; };
export type ShellResizeMessage = { export type ShellResizeMessage = {
@@ -71,8 +69,8 @@ export type UseShellRuntimeResult = {
isConnecting: boolean; isConnecting: boolean;
authUrl: string; authUrl: string;
authUrlVersion: number; authUrlVersion: number;
connectToShell: (options?: { forceRestart?: boolean }) => void; connectToShell: () => void;
disconnectFromShell: (options?: { suppressAutoConnect?: boolean }) => void; disconnectFromShell: () => void;
openAuthUrlInBrowser: (url?: string) => boolean; openAuthUrlInBrowser: (url?: string) => boolean;
copyAuthUrlToClipboard: (url?: string) => Promise<boolean>; copyAuthUrlToClipboard: (url?: string) => Promise<boolean>;
}; };

View File

@@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import '@xterm/xterm/css/xterm.css'; import '@xterm/xterm/css/xterm.css';
import type { Project, ProjectSession } from '../../../types/app'; import type { Project, ProjectSession } from '../../../types/app';
import { import {
@@ -14,7 +13,6 @@ import {
import { useShellRuntime } from '../hooks/useShellRuntime'; import { useShellRuntime } from '../hooks/useShellRuntime';
import { sendSocketMessage } from '../utils/socket'; import { sendSocketMessage } from '../utils/socket';
import { getSessionDisplayName } from '../utils/auth'; import { getSessionDisplayName } from '../utils/auth';
import ShellConnectionOverlay from './subcomponents/ShellConnectionOverlay'; import ShellConnectionOverlay from './subcomponents/ShellConnectionOverlay';
import ShellEmptyState from './subcomponents/ShellEmptyState'; import ShellEmptyState from './subcomponents/ShellEmptyState';
import ShellHeader from './subcomponents/ShellHeader'; import ShellHeader from './subcomponents/ShellHeader';
@@ -48,8 +46,6 @@ export default function Shell({
const [isRestarting, setIsRestarting] = useState(false); const [isRestarting, setIsRestarting] = useState(false);
const [cliPromptOptions, setCliPromptOptions] = useState<CliPromptOption[] | null>(null); const [cliPromptOptions, setCliPromptOptions] = useState<CliPromptOption[] | null>(null);
const promptCheckTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const promptCheckTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const restartTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const restartAfterInitRef = useRef(false);
const onOutputRef = useRef<(() => void) | null>(null); const onOutputRef = useRef<(() => void) | null>(null);
const { const {
@@ -144,7 +140,6 @@ export default function Shell({
useEffect(() => { useEffect(() => {
return () => { return () => {
if (promptCheckTimer.current) clearTimeout(promptCheckTimer.current); if (promptCheckTimer.current) clearTimeout(promptCheckTimer.current);
if (restartTimerRef.current) clearTimeout(restartTimerRef.current);
}; };
}, []); }, []);
@@ -195,42 +190,12 @@ export default function Shell({
); );
const handleRestartShell = useCallback(() => { const handleRestartShell = useCallback(() => {
restartAfterInitRef.current = true;
setIsRestarting(true); setIsRestarting(true);
if (restartTimerRef.current) { window.setTimeout(() => {
clearTimeout(restartTimerRef.current);
}
restartTimerRef.current = setTimeout(() => {
setIsRestarting(false); setIsRestarting(false);
restartTimerRef.current = null;
}, SHELL_RESTART_DELAY_MS); }, SHELL_RESTART_DELAY_MS);
}, []); }, []);
const handleDisconnectShell = useCallback(() => {
restartAfterInitRef.current = false;
if (restartTimerRef.current) {
clearTimeout(restartTimerRef.current);
restartTimerRef.current = null;
}
setIsRestarting(false);
disconnectFromShell({ suppressAutoConnect: true });
}, [disconnectFromShell]);
useEffect(() => {
if (
!restartAfterInitRef.current ||
isRestarting ||
!isInitialized ||
isConnected ||
isConnecting
) {
return;
}
restartAfterInitRef.current = false;
connectToShell({ forceRestart: true });
}, [connectToShell, isConnected, isConnecting, isInitialized, isRestarting]);
if (!selectedProject) { if (!selectedProject) {
return ( return (
<ShellEmptyState <ShellEmptyState
@@ -289,7 +254,7 @@ export default function Shell({
isRestarting={isRestarting} isRestarting={isRestarting}
hasSession={Boolean(selectedSession)} hasSession={Boolean(selectedSession)}
sessionDisplayNameShort={sessionDisplayNameShort} sessionDisplayNameShort={sessionDisplayNameShort}
onDisconnect={handleDisconnectShell} onDisconnect={disconnectFromShell}
onRestart={handleRestartShell} onRestart={handleRestartShell}
statusNewSessionText={t('shell.status.newSession')} statusNewSessionText={t('shell.status.newSession')}
statusInitializingText={t('shell.status.initializing')} statusInitializingText={t('shell.status.initializing')}
@@ -298,7 +263,7 @@ export default function Shell({
disconnectTitle={t('shell.actions.disconnectTitle')} disconnectTitle={t('shell.actions.disconnectTitle')}
restartLabel={t('shell.actions.restart')} restartLabel={t('shell.actions.restart')}
restartTitle={t('shell.actions.restartTitle')} restartTitle={t('shell.actions.restartTitle')}
disableRestart={isRestarting || !isInitialized} disableRestart={isRestarting || isConnected}
/> />
<div className="relative flex-1 overflow-hidden p-2"> <div className="relative flex-1 overflow-hidden p-2">
@@ -316,7 +281,7 @@ export default function Shell({
connectLabel={t('shell.actions.connect')} connectLabel={t('shell.actions.connect')}
connectTitle={t('shell.actions.connectTitle')} connectTitle={t('shell.actions.connectTitle')}
connectingLabel={t('shell.connecting')} connectingLabel={t('shell.connecting')}
onConnect={handleRestartShell} onConnect={connectToShell}
/> />
)} )}

View File

@@ -1,5 +1,3 @@
import { Loader2, RotateCcw } from 'lucide-react';
type ShellConnectionOverlayProps = { type ShellConnectionOverlayProps = {
mode: 'loading' | 'connect' | 'connecting'; mode: 'loading' | 'connect' | 'connecting';
description: string; description: string;
@@ -21,42 +19,40 @@ export default function ShellConnectionOverlay({
}: ShellConnectionOverlayProps) { }: ShellConnectionOverlayProps) {
if (mode === 'loading') { if (mode === 'loading') {
return ( return (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-gray-950/90"> <div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90">
<div className="inline-flex items-center gap-2 text-sm font-medium text-gray-100"> <div className="text-white">{loadingLabel}</div>
<Loader2 className="h-4 w-4 animate-spin text-blue-300" aria-hidden="true" />
<span>{loadingLabel}</span>
</div>
</div> </div>
); );
} }
if (mode === 'connect') { if (mode === 'connect') {
return ( return (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-gray-950/90 p-6"> <div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90 p-4">
<div className="flex w-full max-w-md flex-col items-center gap-3 text-center"> <div className="w-full max-w-sm text-center">
<button <button
type="button"
onClick={onConnect} onClick={onConnect}
className="pointer-events-auto inline-flex min-h-12 w-full max-w-xs cursor-pointer items-center justify-center gap-2 rounded-md bg-emerald-600 px-5 py-3 text-base font-semibold text-white shadow-lg shadow-emerald-950/30 transition-colors hover:bg-emerald-500 focus:outline-none focus:ring-2 focus:ring-emerald-300 focus:ring-offset-2 focus:ring-offset-gray-950 active:bg-emerald-700" className="flex w-full items-center justify-center space-x-2 rounded-lg bg-green-600 px-6 py-3 text-base font-medium text-white transition-colors hover:bg-green-700 sm:w-auto"
title={connectTitle} title={connectTitle}
> >
<RotateCcw className="h-4 w-4" aria-hidden="true" /> <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<span className="min-w-0 truncate">{connectLabel}</span> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span>{connectLabel}</span>
</button> </button>
<p className="max-w-md break-words px-2 text-sm leading-6 text-gray-300">{description}</p> <p className="mt-3 px-2 text-sm text-gray-400">{description}</p>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-gray-950/90 p-6"> <div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90 p-4">
<div className="flex w-full max-w-md flex-col items-center gap-3 text-center"> <div className="w-full max-w-sm text-center">
<div className="flex items-center justify-center gap-3 text-yellow-300"> <div className="flex items-center justify-center space-x-3 text-yellow-400">
<Loader2 className="h-5 w-5 animate-spin" aria-hidden="true" /> <div className="h-6 w-6 animate-spin rounded-full border-2 border-yellow-400 border-t-transparent"></div>
<span className="text-base font-medium">{connectingLabel}</span> <span className="text-base font-medium">{connectingLabel}</span>
</div> </div>
<p className="max-w-md break-words px-2 text-sm leading-6 text-gray-300">{description}</p> <p className="mt-3 px-2 text-sm text-gray-400">{description}</p>
</div> </div>
</div> </div>
); );

View File

@@ -1,5 +1,3 @@
import { RotateCcw, X } from 'lucide-react';
type ShellHeaderProps = { type ShellHeaderProps = {
isConnected: boolean; isConnected: boolean;
isInitialized: boolean; isInitialized: boolean;
@@ -52,27 +50,34 @@ export default function ShellHeader({
{isRestarting && <span className="text-xs text-blue-400">{statusRestartingText}</span>} {isRestarting && <span className="text-xs text-blue-400">{statusRestartingText}</span>}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center space-x-3">
{isConnected && ( {isConnected && (
<button <button
type="button"
onClick={onDisconnect} onClick={onDisconnect}
className="inline-flex h-8 items-center gap-1.5 rounded-md bg-red-600 px-3 text-xs font-medium text-white transition-colors hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400/70 focus:ring-offset-2 focus:ring-offset-gray-800" className="flex items-center space-x-1 rounded bg-red-600 px-3 py-1 text-xs text-white hover:bg-red-700"
title={disconnectTitle} title={disconnectTitle}
> >
<X className="h-3.5 w-3.5" aria-hidden="true" /> <svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
<span>{disconnectLabel}</span> <span>{disconnectLabel}</span>
</button> </button>
)} )}
<button <button
type="button"
onClick={onRestart} onClick={onRestart}
disabled={disableRestart} disabled={disableRestart}
className="inline-flex h-8 items-center gap-1.5 rounded-md border border-gray-600/80 bg-gray-700/70 px-3 text-xs font-medium text-gray-100 transition-colors hover:border-blue-400/70 hover:bg-blue-600/80 hover:text-white focus:outline-none focus:ring-2 focus:ring-blue-400/70 focus:ring-offset-2 focus:ring-offset-gray-800 disabled:cursor-not-allowed disabled:border-transparent disabled:bg-transparent disabled:text-gray-500 disabled:opacity-60" className="flex items-center space-x-1 text-xs text-gray-400 hover:text-white disabled:cursor-not-allowed disabled:opacity-50"
title={restartTitle} title={restartTitle}
> >
<RotateCcw className={`h-3.5 w-3.5 ${isRestarting ? 'animate-spin' : ''}`} aria-hidden="true" /> <svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
<span>{restartLabel}</span> <span>{restartLabel}</span>
</button> </button>
</div> </div>

View File

@@ -84,15 +84,6 @@ import itCodeEditor from './locales/it/codeEditor.json';
// eslint-disable-next-line import-x/order // eslint-disable-next-line import-x/order
import itTasks from './locales/it/tasks.json'; import itTasks from './locales/it/tasks.json';
import zhTWCommon from './locales/zh-TW/common.json';
import zhTWSettings from './locales/zh-TW/settings.json';
import zhTWAuth from './locales/zh-TW/auth.json';
import zhTWSidebar from './locales/zh-TW/sidebar.json';
import zhTWChat from './locales/zh-TW/chat.json';
import zhTWCodeEditor from './locales/zh-TW/codeEditor.json';
// eslint-disable-next-line import-x/order
import zhTWTasks from './locales/zh-TW/tasks.json';
// Import supported languages configuration // Import supported languages configuration
import { languages } from './languages.js'; import { languages } from './languages.js';
@@ -187,15 +178,6 @@ i18n
codeEditor: itCodeEditor, codeEditor: itCodeEditor,
tasks: itTasks, tasks: itTasks,
}, },
'zh-TW': {
common: zhTWCommon,
settings: zhTWSettings,
auth: zhTWAuth,
sidebar: zhTWSidebar,
chat: zhTWChat,
codeEditor: zhTWCodeEditor,
tasks: zhTWTasks,
},
}, },
// Default language // Default language

View File

@@ -24,11 +24,6 @@ export const languages = [
label: 'Simplified Chinese', label: 'Simplified Chinese',
nativeName: '简体中文', nativeName: '简体中文',
}, },
{
value: 'zh-TW',
label: 'Traditional Chinese',
nativeName: '繁體中文',
},
{ {
value: 'ja', value: 'ja',
label: 'Japanese', label: 'Japanese',

View File

@@ -138,6 +138,42 @@
"clearInput": "Eingabe leeren", "clearInput": "Eingabe leeren",
"scrollToBottom": "Nach unten scrollen" "scrollToBottom": "Nach unten scrollen"
}, },
"thinkingMode": {
"selector": {
"title": "Denkmodus",
"description": "Erweitertes Denken gibt Claude mehr Zeit, Alternativen zu evaluieren",
"active": "Aktiv",
"tip": "Höhere Denkmodi brauchen mehr Zeit, liefern aber eine gründlichere Analyse"
},
"modes": {
"none": {
"name": "Standard",
"description": "Reguläre Claude-Antwort",
"prefix": ""
},
"think": {
"name": "Denken",
"description": "Grundlegendes erweitertes Denken",
"prefix": "think"
},
"thinkHard": {
"name": "Intensiv denken",
"description": "Gründlichere Auswertung",
"prefix": "think hard"
},
"thinkHarder": {
"name": "Sehr intensiv denken",
"description": "Tiefgehende Analyse mit Alternativen",
"prefix": "think harder"
},
"ultrathink": {
"name": "Ultradenken",
"description": "Maximales Denkbudget",
"prefix": "ultrathink"
}
},
"buttonTitle": "Denkmodus: {{mode}}"
},
"providerSelection": { "providerSelection": {
"title": "KI-Assistent wählen", "title": "KI-Assistent wählen",
"description": "Anbieter auswählen, um eine neue Unterhaltung zu starten", "description": "Anbieter auswählen, um eine neue Unterhaltung zu starten",

View File

@@ -139,6 +139,42 @@
"clearInput": "Clear input", "clearInput": "Clear input",
"scrollToBottom": "Scroll to bottom" "scrollToBottom": "Scroll to bottom"
}, },
"thinkingMode": {
"selector": {
"title": "Thinking Mode",
"description": "Extended thinking gives Claude more time to evaluate alternatives",
"active": "Active",
"tip": "Higher thinking modes take more time but provide more thorough analysis"
},
"modes": {
"none": {
"name": "Standard",
"description": "Regular Claude response",
"prefix": ""
},
"think": {
"name": "Think",
"description": "Basic extended thinking",
"prefix": "think"
},
"thinkHard": {
"name": "Think Hard",
"description": "More thorough evaluation",
"prefix": "think hard"
},
"thinkHarder": {
"name": "Think Harder",
"description": "Deep analysis with alternatives",
"prefix": "think harder"
},
"ultrathink": {
"name": "Ultrathink",
"description": "Maximum thinking budget",
"prefix": "ultrathink"
}
},
"buttonTitle": "Thinking mode: {{mode}}"
},
"providerSelection": { "providerSelection": {
"title": "Choose Your AI Assistant", "title": "Choose Your AI Assistant",
"description": "Select a provider to start a new conversation", "description": "Select a provider to start a new conversation",
@@ -193,7 +229,7 @@
"disconnect": "Disconnect", "disconnect": "Disconnect",
"disconnectTitle": "Disconnect from shell", "disconnectTitle": "Disconnect from shell",
"restart": "Restart", "restart": "Restart",
"restartTitle": "Restart Shell", "restartTitle": "Restart Shell (disconnect first)",
"connect": "Continue in Shell", "connect": "Continue in Shell",
"connectTitle": "Connect to shell" "connectTitle": "Connect to shell"
}, },

View File

@@ -138,6 +138,42 @@
"clearInput": "Cancella input", "clearInput": "Cancella input",
"scrollToBottom": "Scorri in basso" "scrollToBottom": "Scorri in basso"
}, },
"thinkingMode": {
"selector": {
"title": "Modalità ragionamento",
"description": "Il ragionamento esteso dà a Claude più tempo per valutare le alternative",
"active": "Attivo",
"tip": "Modalità di ragionamento più elevate richiedono più tempo ma forniscono un'analisi più approfondita"
},
"modes": {
"none": {
"name": "Standard",
"description": "Risposta Claude normale",
"prefix": ""
},
"think": {
"name": "Pensa",
"description": "Ragionamento esteso base",
"prefix": "think"
},
"thinkHard": {
"name": "Pensa di più",
"description": "Valutazione più approfondita",
"prefix": "think hard"
},
"thinkHarder": {
"name": "Pensa ancora",
"description": "Analisi profonda con alternative",
"prefix": "think harder"
},
"ultrathink": {
"name": "Ultrapensiero",
"description": "Budget massimo di ragionamento",
"prefix": "ultrathink"
}
},
"buttonTitle": "Modalità ragionamento: {{mode}}"
},
"providerSelection": { "providerSelection": {
"title": "Scegli il tuo assistente AI", "title": "Scegli il tuo assistente AI",
"description": "Seleziona un provider per iniziare una nuova conversazione", "description": "Seleziona un provider per iniziare una nuova conversazione",

View File

@@ -117,6 +117,42 @@
"clickToChangeMode": "クリックで権限モードを変更または入力欄でTab", "clickToChangeMode": "クリックで権限モードを変更または入力欄でTab",
"showAllCommands": "すべてのコマンドを表示" "showAllCommands": "すべてのコマンドを表示"
}, },
"thinkingMode": {
"selector": {
"title": "思考モード",
"description": "拡張思考によりClaudeがより多くの選択肢を検討できます",
"active": "有効",
"tip": "高い思考モードは時間がかかりますが、より深い分析が得られます"
},
"modes": {
"none": {
"name": "標準",
"description": "通常のClaudeの応答",
"prefix": ""
},
"think": {
"name": "Think",
"description": "基本的な拡張思考",
"prefix": "think"
},
"thinkHard": {
"name": "Think Hard",
"description": "より深い検討",
"prefix": "think hard"
},
"thinkHarder": {
"name": "Think Harder",
"description": "代替案を含む深い分析",
"prefix": "think harder"
},
"ultrathink": {
"name": "Ultrathink",
"description": "最大限の思考予算",
"prefix": "ultrathink"
}
},
"buttonTitle": "思考モード: {{mode}}"
},
"providerSelection": { "providerSelection": {
"title": "AIアシスタントを選択", "title": "AIアシスタントを選択",
"description": "新しい会話を始めるプロバイダーを選択してください", "description": "新しい会話を始めるプロバイダーを選択してください",

View File

@@ -120,6 +120,42 @@
"clearInput": "입력 지우기", "clearInput": "입력 지우기",
"scrollToBottom": "맨 아래로 스크롤" "scrollToBottom": "맨 아래로 스크롤"
}, },
"thinkingMode": {
"selector": {
"title": "Thinking 모드",
"description": "확장된 thinking은 Claude에게 대안을 평가할 시간을 더 줍니다",
"active": "활성",
"tip": "높은 thinking 모드는 시간이 더 걸리지만 더 철저한 분석을 제공합니다"
},
"modes": {
"none": {
"name": "Standard",
"description": "일반 Claude 응답",
"prefix": ""
},
"think": {
"name": "Think",
"description": "기본 확장 thinking",
"prefix": "think"
},
"thinkHard": {
"name": "Think Hard",
"description": "더 철저한 평가",
"prefix": "think hard"
},
"thinkHarder": {
"name": "Think Harder",
"description": "대안을 포함한 심층 분석",
"prefix": "think harder"
},
"ultrathink": {
"name": "Ultrathink",
"description": "최대 thinking 예산",
"prefix": "ultrathink"
}
},
"buttonTitle": "Thinking 모드: {{mode}}"
},
"providerSelection": { "providerSelection": {
"title": "AI 어시스턴트 선택", "title": "AI 어시스턴트 선택",
"description": "새 대화를 시작할 프로바이더를 선택하세요", "description": "새 대화를 시작할 프로바이더를 선택하세요",

View File

@@ -138,6 +138,42 @@
"clearInput": "Очистить ввод", "clearInput": "Очистить ввод",
"scrollToBottom": "Прокрутить вниз" "scrollToBottom": "Прокрутить вниз"
}, },
"thinkingMode": {
"selector": {
"title": "Режим размышления",
"description": "Расширенное размышление дает Claude больше времени для оценки альтернатив",
"active": "Активен",
"tip": "Более высокие режимы размышления занимают больше времени, но обеспечивают более тщательный анализ"
},
"modes": {
"none": {
"name": "Стандартный",
"description": "Обычный ответ Claude",
"prefix": ""
},
"think": {
"name": "Думать",
"description": "Базовое расширенное размышление",
"prefix": "думать"
},
"thinkHard": {
"name": "Думать усердно",
"description": "Более тщательная оценка",
"prefix": "думать усердно"
},
"thinkHarder": {
"name": "Думать еще усерднее",
"description": "Глубокий анализ с альтернативами",
"prefix": "думать еще усерднее"
},
"ultrathink": {
"name": "Ультра-размышление",
"description": "Максимальный бюджет размышления",
"prefix": "ультра-размышление"
}
},
"buttonTitle": "Режим размышления: {{mode}}"
},
"providerSelection": { "providerSelection": {
"title": "Выберите вашего AI-ассистента", "title": "Выберите вашего AI-ассистента",
"description": "Выберите провайдера для начала нового разговора", "description": "Выберите провайдера для начала нового разговора",

View File

@@ -138,6 +138,42 @@
"clearInput": "Girdiyi temizle", "clearInput": "Girdiyi temizle",
"scrollToBottom": "En alta git" "scrollToBottom": "En alta git"
}, },
"thinkingMode": {
"selector": {
"title": "Düşünme Modu",
"description": "Uzatılmış düşünme, Claude'a alternatifleri değerlendirmek için daha fazla zaman verir",
"active": "Aktif",
"tip": "Daha yüksek düşünme modları daha fazla zaman alır ama daha kapsamlı analiz sağlar"
},
"modes": {
"none": {
"name": "Standart",
"description": "Normal Claude yanıtı",
"prefix": ""
},
"think": {
"name": "Düşün",
"description": "Temel uzatılmış düşünme",
"prefix": "think"
},
"thinkHard": {
"name": "Daha Fazla Düşün",
"description": "Daha kapsamlı değerlendirme",
"prefix": "think hard"
},
"thinkHarder": {
"name": "Derin Düşün",
"description": "Alternatiflerle derin analiz",
"prefix": "think harder"
},
"ultrathink": {
"name": "Ultra Düşün",
"description": "Maksimum düşünme bütçesi",
"prefix": "ultrathink"
}
},
"buttonTitle": "Düşünme modu: {{mode}}"
},
"providerSelection": { "providerSelection": {
"title": "AI Asistanını Seç", "title": "AI Asistanını Seç",
"description": "Yeni bir konuşma başlatmak için bir sağlayıcı seç", "description": "Yeni bir konuşma başlatmak için bir sağlayıcı seç",

View File

@@ -120,6 +120,42 @@
"clearInput": "清空输入", "clearInput": "清空输入",
"scrollToBottom": "滚动到底部" "scrollToBottom": "滚动到底部"
}, },
"thinkingMode": {
"selector": {
"title": "思考模式",
"description": "扩展思考给 Claude 更多时间来评估替代方案",
"active": "激活",
"tip": "更高的思考模式需要更多时间,但提供更彻底的分析"
},
"modes": {
"none": {
"name": "标准",
"description": "常规 Claude 响应",
"prefix": ""
},
"think": {
"name": "思考",
"description": "基本扩展思考",
"prefix": "思考"
},
"thinkHard": {
"name": "深入思考",
"description": "更彻底的评估",
"prefix": "深入思考"
},
"thinkHarder": {
"name": "更深入思考",
"description": "考虑替代方案的深度分析",
"prefix": "更深入思考"
},
"ultrathink": {
"name": "超级思考",
"description": "最大思考预算",
"prefix": "超级思考"
}
},
"buttonTitle": "思考模式:{{mode}}"
},
"providerSelection": { "providerSelection": {
"title": "选择您的 AI 助手", "title": "选择您的 AI 助手",
"description": "选择一个供应商以开始新对话", "description": "选择一个供应商以开始新对话",

View File

@@ -1,37 +0,0 @@
{
"login": {
"title": "歡迎回來",
"description": "登入您的 CloudCLI 帳戶",
"username": "使用者名稱",
"password": "密碼",
"submit": "登入",
"loading": "登入中...",
"errors": {
"invalidCredentials": "使用者名稱或密碼無效",
"requiredFields": "請填寫所有欄位",
"networkError": "網路錯誤,請重試。"
},
"placeholders": {
"username": "輸入您的使用者名稱",
"password": "輸入您的密碼"
}
},
"register": {
"title": "建立帳戶",
"username": "使用者名稱",
"password": "密碼",
"confirmPassword": "確認密碼",
"submit": "建立帳戶",
"loading": "建立帳戶中...",
"errors": {
"passwordMismatch": "密碼不一致",
"usernameTaken": "使用者名稱已被使用",
"weakPassword": "密碼強度太弱"
}
},
"logout": {
"title": "登出",
"confirm": "確定要登出嗎?",
"button": "登出"
}
}

View File

@@ -1,257 +0,0 @@
{
"codeBlock": {
"copy": "複製",
"copied": "已複製",
"copyCode": "複製程式碼"
},
"copyMessage": {
"copy": "複製訊息",
"copied": "訊息已複製",
"selectFormat": "選擇複製格式",
"copyAsMarkdown": "複製為 Markdown",
"copyAsText": "複製為純文字"
},
"messageTypes": {
"user": "U",
"error": "錯誤",
"tool": "工具",
"claude": "Claude",
"cursor": "Cursor",
"codex": "Codex",
"gemini": "Gemini"
},
"tools": {
"settings": "工具設定",
"error": "工具錯誤",
"result": "工具結果",
"viewParams": "查看輸入參數",
"viewRawParams": "查看原始參數",
"viewDiff": "查看編輯差異",
"creatingFile": "建立新檔案:",
"updatingTodo": "更新待辦事項",
"read": "讀取",
"readFile": "讀取檔案",
"updateTodo": "更新待辦清單",
"readTodo": "讀取待辦清單",
"searchResults": "結果"
},
"search": {
"found": "找到 {{count}} 個{{type}}",
"file": "檔案",
"files": "檔案",
"pattern": "模式:",
"in": "在:"
},
"fileOperations": {
"updated": "檔案更新成功",
"created": "檔案建立成功",
"written": "檔案寫入成功",
"diff": "差異",
"newFile": "新檔案",
"viewContent": "查看檔案內容",
"viewFullOutput": "查看完整輸出({{count}} 個字元)",
"contentDisplayed": "檔案內容顯示在上方的差異檢視中"
},
"interactive": {
"title": "互動式提示",
"waiting": "等待您在 CLI 中回應",
"instruction": "請在 Claude 執行的終端機中選擇一個選項。",
"selectedOption": "✓ Claude 選擇了選項 {{number}}",
"instructionDetail": "在 CLI 中,您可以使用方向鍵或輸入數字來互動式地選擇此選項。"
},
"thinking": {
"title": "思考中...",
"emoji": "💭 思考中..."
},
"json": {
"response": "JSON 回應"
},
"permissions": {
"grant": "授予 {{tool}} 權限",
"added": "權限已新增",
"addTo": "將 {{entry}} 加入允許的工具。",
"retry": "權限已儲存。重試請求以使用該工具。",
"error": "無法更新權限。請重試。",
"openSettings": "開啟設定"
},
"todo": {
"updated": "待辦清單已成功更新",
"current": "目前待辦清單"
},
"plan": {
"viewPlan": "📋 查看實作計畫",
"title": "實作計畫"
},
"usageLimit": {
"resetAt": "Claude 使用限制已達到。您的限制將在 **{{time}} {{timezone}}** - {{date}} 重置"
},
"codex": {
"permissionMode": "權限模式",
"modes": {
"default": "預設模式",
"auto": "自動模式",
"acceptEdits": "編輯模式",
"bypassPermissions": "無限制模式",
"plan": "計畫模式"
},
"descriptions": {
"default": "只有受信任的指令ls、cat、grep、git status 等)自動執行。其他指令將被略過。可以寫入工作區。",
"auto": "由模型分類器針對每次工具呼叫決定核准或拒絕。免手動操作,但比 Bypass 安全——仍可能發生拒絕。",
"acceptEdits": "工作區內的所有指令自動執行。完全自動模式,具有沙箱執行功能。",
"bypassPermissions": "完全的系統存取,無限制。所有指令自動執行,具有完整的磁碟和網路存取權限。請謹慎使用。",
"plan": "計畫模式 - 不執行任何指令"
},
"technicalDetails": "技術細節"
},
"input": {
"placeholder": "輸入 / 叫用指令,@ 選取檔案,或向 {{provider}} 提問...",
"placeholderDefault": "輸入您的訊息...",
"disabled": "輸入已停用",
"attachFiles": "附加檔案",
"attachImages": "附加圖片",
"send": "傳送",
"stop": "停止",
"hintText": {
"ctrlEnter": "Ctrl+Enter 傳送 • Shift+Enter 換行 • Tab 切換模式 • / 斜線指令",
"enter": "Enter 傳送 • Shift+Enter 換行 • Tab 切換模式 • / 斜線指令"
},
"clickToChangeMode": "點擊變更權限模式(或在輸入框中按 Tab",
"showAllCommands": "顯示所有指令",
"clearInput": "清空輸入",
"scrollToBottom": "捲動到底部"
},
"thinkingMode": {
"selector": {
"title": "思考模式",
"description": "延伸思考讓 Claude 有更多時間來評估替代方案",
"active": "啟用",
"tip": "更高的思考模式需要更多時間,但提供更徹底的分析"
},
"modes": {
"none": {
"name": "標準",
"description": "一般 Claude 回應",
"prefix": ""
},
"think": {
"name": "思考",
"description": "基本延伸思考",
"prefix": "思考"
},
"thinkHard": {
"name": "深入思考",
"description": "更徹底的評估",
"prefix": "深入思考"
},
"thinkHarder": {
"name": "更深入思考",
"description": "考慮替代方案的深度分析",
"prefix": "更深入思考"
},
"ultrathink": {
"name": "超級思考",
"description": "最大思考預算",
"prefix": "超級思考"
}
},
"buttonTitle": "思考模式:{{mode}}"
},
"providerSelection": {
"title": "選擇您的 AI 助手",
"description": "選擇一個提供者以開始新對話",
"selectModel": "選擇模型",
"providerInfo": {
"anthropic": "由 Anthropic 提供",
"openai": "由 OpenAI 提供",
"cursorEditor": "AI 程式碼編輯器",
"google": "由 Google 提供"
},
"readyPrompt": {
"claude": "準備好使用 {{model}} 的 Claude。請在下方開始輸入您的訊息。",
"cursor": "準備好使用 {{model}} 的 Cursor。請在下方開始輸入您的訊息。",
"codex": "準備好使用 {{model}} 的 Codex。請在下方開始輸入您的訊息。",
"gemini": "準備好使用 {{model}} 的 Gemini。請在下方開始輸入您的訊息。",
"default": "請在上方選擇一個提供者以開始"
},
"pressToSearch": "按 <kbd>{{shortcut}}</kbd> 搜尋工作階段、檔案和提交"
},
"session": {
"continue": {
"title": "繼續您的對話",
"description": "詢問有關程式碼的問題、要求修改或取得開發任務的協助"
},
"loading": {
"olderMessages": "正在載入較早的訊息...",
"sessionMessages": "正在載入工作階段訊息..."
},
"messages": {
"showingOf": "顯示 {{shown}} / {{total}} 則訊息",
"scrollToLoad": "向上捲動以載入更多",
"showingLast": "顯示最近 {{count}} 則訊息(共 {{total}} 則)",
"loadEarlier": "載入較早的訊息",
"loadAll": "載入全部訊息",
"loadingAll": "正在載入全部訊息...",
"allLoaded": "全部訊息已載入",
"perfWarning": "已載入全部訊息 - 捲動可能變慢。點擊「捲動到底部」恢復效能。"
}
},
"shell": {
"selectProject": {
"title": "選擇專案",
"description": "選擇一個專案以在該目錄中開啟互動式 Shell"
},
"status": {
"newSession": "新工作階段",
"initializing": "初始化中...",
"restarting": "重新啟動中..."
},
"actions": {
"disconnect": "中斷連線",
"disconnectTitle": "中斷 Shell 連線",
"restart": "重新啟動",
"restartTitle": "重新啟動 Shell請先中斷連線",
"connect": "在 Shell 中繼續",
"connectTitle": "連線到 Shell"
},
"loading": "正在載入終端機...",
"connecting": "正在連線到 Shell...",
"startSession": "啟動新的 Claude 工作階段",
"resumeSession": "恢復工作階段:{{displayName}}...",
"runCommand": "在 {{projectName}} 中執行 {{command}}",
"startCli": "在 {{projectName}} 中啟動 Claude CLI",
"defaultCommand": "指令"
},
"claudeStatus": {
"actions": {
"thinking": "思考中",
"processing": "處理中",
"analyzing": "分析中",
"working": "執行中",
"computing": "運算中",
"reasoning": "推理中"
},
"state": {
"live": "進行中",
"paused": "已暫停"
},
"elapsed": {
"seconds": "{{count}} 秒",
"minutesSeconds": "{{minutes}} 分 {{seconds}} 秒",
"label": "已過 {{time}}",
"startingNow": "正要開始"
},
"controls": {
"stopGeneration": "停止生成",
"pressEscToStop": "隨時按 Esc 即可停止"
},
"providers": {
"assistant": "助理"
}
},
"projectSelection": {
"startChatWithProvider": "選擇一個專案以開始與 {{provider}} 聊天"
},
"tasks": {
"nextTaskPrompt": "開始下一個任務"
}
}

View File

@@ -1,34 +0,0 @@
{
"toolbar": {
"changes": "個變更",
"previousChange": "上一個變更",
"nextChange": "下一個變更",
"hideDiff": "隱藏差異醒目提示",
"showDiff": "顯示差異醒目提示",
"settings": "編輯器設定",
"collapse": "收合編輯器",
"expand": "展開編輯器到全寬"
},
"loading": "正在載入 {{fileName}}...",
"header": {
"showingChanges": "顯示變更"
},
"actions": {
"download": "下載檔案",
"save": "儲存",
"saving": "儲存中...",
"saved": "已儲存!",
"exitFullscreen": "離開全螢幕",
"fullscreen": "全螢幕",
"close": "關閉"
},
"footer": {
"lines": "行數:",
"characters": "字元數:",
"shortcuts": "按 Ctrl+S 儲存 • Esc 關閉"
},
"binaryFile": {
"title": "二進位檔案",
"message": "檔案「{{fileName}}」無法在文字編輯器中顯示,因為它是二進位檔案。"
}
}

View File

@@ -1,267 +0,0 @@
{
"buttons": {
"save": "儲存",
"cancel": "取消",
"delete": "刪除",
"create": "建立",
"edit": "編輯",
"close": "關閉",
"confirm": "確認",
"submit": "送出",
"retry": "重試",
"refresh": "重新整理",
"search": "搜尋",
"clear": "清除",
"copy": "複製",
"download": "下載",
"upload": "上傳",
"browse": "瀏覽"
},
"tabs": {
"chat": "聊天",
"shell": "終端機",
"files": "檔案",
"git": "版本控制",
"tasks": "任務"
},
"status": {
"loading": "載入中...",
"success": "成功",
"error": "錯誤",
"failed": "失敗",
"pending": "待處理",
"completed": "已完成",
"inProgress": "進行中"
},
"messages": {
"savedSuccessfully": "儲存成功",
"deletedSuccessfully": "刪除成功",
"updatedSuccessfully": "更新成功",
"operationFailed": "操作失敗",
"networkError": "網路錯誤,請檢查您的連線。",
"unauthorized": "未授權,請登入。",
"notFound": "找不到",
"invalidInput": "輸入無效",
"requiredField": "此欄位為必填",
"unknownError": "發生未知錯誤"
},
"navigation": {
"settings": "設定",
"home": "首頁",
"back": "返回",
"next": "下一步",
"previous": "上一步",
"logout": "登出"
},
"common": {
"language": "語言",
"theme": "佈景主題",
"darkMode": "深色模式",
"lightMode": "淺色模式",
"name": "名稱",
"description": "描述",
"enabled": "已啟用",
"disabled": "已停用",
"optional": "選填",
"version": "版本",
"select": "選取",
"selectAll": "全選",
"deselectAll": "取消全選"
},
"time": {
"justNow": "剛剛",
"minutesAgo": "{{count}} 分鐘前",
"hoursAgo": "{{count}} 小時前",
"daysAgo": "{{count}} 天前",
"yesterday": "昨天"
},
"fileOperations": {
"newFile": "新增檔案",
"newFolder": "新增資料夾",
"rename": "重新命名",
"move": "移動",
"copyPath": "複製路徑",
"openInEditor": "在編輯器中開啟"
},
"mainContent": {
"loading": "正在載入 CloudCLI",
"settingUpWorkspace": "正在設定您的工作區...",
"chooseProject": "選擇您的專案",
"selectProjectDescription": "從側邊欄選擇一個專案以開始使用 Claude 進行程式開發。每個專案包含您的聊天紀錄和檔案歷史。",
"tip": "提示",
"createProjectMobile": "點擊上方的選單按鈕以存取專案",
"createProjectDesktop": "點擊側邊欄中的資料夾圖示以建立新專案",
"newSession": "新工作階段",
"untitledSession": "未命名工作階段",
"projectFiles": "專案檔案"
},
"fileTree": {
"loading": "正在載入檔案...",
"files": "檔案",
"simpleView": "簡易檢視",
"compactView": "精簡檢視",
"detailedView": "詳細檢視",
"searchPlaceholder": "搜尋檔案和資料夾...",
"clearSearch": "清除搜尋",
"name": "名稱",
"size": "大小",
"modified": "修改時間",
"permissions": "權限",
"noFilesFound": "找不到檔案",
"checkProjectPath": "請檢查專案路徑是否可存取",
"noMatchesFound": "找不到符合項目",
"tryDifferentSearch": "嘗試不同的搜尋詞或清除搜尋",
"justNow": "剛剛",
"minAgo": "{{count}} 分鐘前",
"hoursAgo": "{{count}} 小時前",
"daysAgo": "{{count}} 天前",
"newFile": "新增檔案 (Cmd+N)",
"newFolder": "新增資料夾 (Cmd+Shift+N)",
"refresh": "重新整理",
"collapseAll": "全部收合",
"context": {
"rename": "重新命名",
"delete": "刪除",
"copyPath": "複製路徑",
"download": "下載",
"newFile": "新增檔案",
"newFolder": "新增資料夾",
"refresh": "重新整理",
"menuLabel": "檔案右鍵選單",
"loading": "載入中..."
}
},
"projectWizard": {
"title": "建立新專案",
"steps": {
"type": "類型",
"configure": "設定",
"confirm": "確認"
},
"step1": {
"question": "您已經有工作區,還是想建立一個新的工作區?",
"existing": {
"title": "現有工作區",
"description": "我的伺服器上已經有工作區,只需要將其加入專案列表"
},
"new": {
"title": "新建工作區",
"description": "建立新工作區,可選擇從 GitHub 儲存庫複製"
}
},
"step2": {
"existingPath": "工作區路徑",
"newPath": "工作區路徑",
"existingPlaceholder": "/path/to/existing/workspace",
"newPlaceholder": "/path/to/new/workspace",
"existingHelp": "您現有工作區目錄的完整路徑",
"newHelp": "工作區目錄的完整路徑",
"githubUrl": "GitHub URL選填",
"githubPlaceholder": "https://github.com/username/repository",
"githubHelp": "選填:提供 GitHub URL 以複製儲存庫",
"githubAuth": "GitHub 身分驗證(選填)",
"githubAuthHelp": "僅私有儲存庫需要。公開儲存庫無需身分驗證即可複製。",
"loadingTokens": "正在載入已儲存的權杖...",
"storedToken": "已儲存的權杖",
"newToken": "新權杖",
"nonePublic": "無(公開)",
"selectToken": "選取權杖",
"selectTokenPlaceholder": "-- 選取權杖 --",
"tokenPlaceholder": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"tokenHelp": "此權杖僅用於此操作",
"publicRepoInfo": "公開儲存庫不需要身分驗證。如果複製公開儲存庫,可以略過提供權杖。",
"noTokensHelp": "沒有可用的已儲存權杖。您可以在 設定 → API 金鑰 中新增權杖以便重複使用。",
"optionalTokenPublic": "GitHub 權杖(公開儲存庫可選)",
"tokenPublicPlaceholder": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx公開儲存庫可留空"
},
"step3": {
"reviewConfig": "檢閱您的設定",
"existingWorkspace": "現有工作區",
"newWorkspace": "新建工作區",
"path": "路徑:",
"cloneFrom": "複製自:",
"authentication": "身分驗證:",
"usingStoredToken": "使用已儲存的權杖:",
"usingProvidedToken": "使用提供的權杖",
"noAuthentication": "無身分驗證",
"sshKey": "SSH 金鑰",
"existingInfo": "工作區將加入您的專案列表,並可用於 Claude/Cursor 工作階段。",
"newWithClone": "儲存庫將從此資料夾複製。",
"newEmpty": "工作區將加入您的專案列表,並可用於 Claude/Cursor 工作階段。",
"cloningRepository": "正在複製儲存庫..."
},
"buttons": {
"cancel": "取消",
"back": "返回",
"next": "下一步",
"createProject": "建立專案",
"creating": "建立中...",
"cloning": "正在複製..."
},
"errors": {
"selectType": "請選擇您已有現有工作區還是想建立新工作區",
"providePath": "請提供工作區路徑",
"failedToCreate": "建立工作區失敗",
"failedToCreateFolder": "建立資料夾失敗"
}
},
"notifications": {
"genericTool": "工具",
"codes": {
"generic": {
"info": {
"title": "通知"
}
},
"permission": {
"required": {
"title": "需要處理",
"body": "{{toolName}} 正在等待你的決定。"
}
},
"run": {
"stopped": {
"title": "執行已停止",
"body": "原因:{{reason}}"
},
"failed": {
"title": "執行失敗"
}
},
"agent": {
"notification": {
"title": "Agent 通知"
}
}
}
},
"versionUpdate": {
"title": "有可用更新",
"newVersionReady": "新版本已準備就緒",
"currentVersion": "目前版本",
"latestVersion": "最新版本",
"whatsNew": "新功能:",
"viewFullRelease": "查看完整發行說明",
"updateProgress": "更新進度:",
"manualUpgrade": "手動升級:",
"npmUpgradeCommand": "npm install -g @cloudcli-ai/cloudcli@latest",
"manualUpgradeHint": "或點擊「立即更新」以自動執行更新。",
"updateCompleted": "更新成功完成!",
"restartServer": "請重新啟動伺服器以套用變更。",
"updateFailed": "更新失敗",
"buttons": {
"close": "關閉",
"later": "稍後",
"copyCommand": "複製指令",
"updateNow": "立即更新",
"updating": "更新中..."
},
"ariaLabels": {
"closeModal": "關閉版本升級對話框",
"showSidebar": "顯示側邊欄",
"settings": "設定",
"updateAvailable": "有可用更新",
"closeSidebar": "關閉側邊欄"
}
}
}

View File

@@ -1,484 +0,0 @@
{
"title": "設定",
"tabs": {
"account": "帳戶",
"permissions": "權限",
"mcpServers": "MCP 伺服器",
"appearance": "外觀"
},
"account": {
"title": "帳戶",
"language": "語言",
"languageLabel": "顯示語言",
"languageDescription": "選擇您偏好的介面語言",
"username": "使用者名稱",
"email": "電子郵件",
"profile": "個人檔案",
"changePassword": "變更密碼"
},
"mcp": {
"title": "MCP 伺服器",
"addServer": "新增伺服器",
"editServer": "編輯伺服器",
"deleteServer": "刪除伺服器",
"serverName": "伺服器名稱",
"serverType": "伺服器類型",
"config": "設定",
"testConnection": "測試連線",
"status": "狀態",
"connected": "已連線",
"disconnected": "未連線",
"scope": {
"label": "範圍",
"user": "使用者",
"project": "專案"
}
},
"appearance": {
"title": "外觀",
"theme": "佈景主題",
"codeEditor": "程式碼編輯器",
"editorTheme": "編輯器佈景主題",
"wordWrap": "自動換行",
"showMinimap": "顯示縮圖",
"lineNumbers": "行號",
"fontSize": "字型大小"
},
"actions": {
"saveChanges": "儲存變更",
"resetToDefaults": "重設為預設值",
"cancelChanges": "取消變更"
},
"quickSettings": {
"title": "快速設定",
"sections": {
"appearance": "外觀",
"toolDisplay": "工具顯示",
"viewOptions": "檢視選項",
"inputSettings": "輸入設定"
},
"darkMode": "深色模式",
"autoExpandTools": "自動展開工具",
"showRawParameters": "顯示原始參數",
"showThinking": "顯示思考過程",
"autoScrollToBottom": "自動捲動到底部",
"sendByCtrlEnter": "使用 Ctrl+Enter 傳送",
"sendByCtrlEnterDescription": "啟用後,按 Ctrl+Enter 傳送訊息,而不是僅按 Enter。這對於使用輸入法的使用者可以避免意外傳送。",
"dragHandle": {
"dragging": "正在拖曳手柄",
"closePanel": "關閉設定面板",
"openPanel": "開啟設定面板",
"draggingStatus": "正在拖曳...",
"toggleAndMove": "點擊切換,拖曳移動"
}
},
"terminalShortcuts": {
"title": "終端機快速鍵",
"sectionKeys": "按鍵",
"sectionNavigation": "導覽",
"escape": "Escape",
"tab": "Tab",
"shiftTab": "Shift+Tab",
"arrowUp": "向上箭頭",
"arrowDown": "向下箭頭",
"scrollDown": "捲動到底部",
"handle": {
"closePanel": "關閉快速鍵面板",
"openPanel": "開啟快速鍵面板"
}
},
"mainTabs": {
"label": "設定",
"agents": "智慧代理",
"appearance": "外觀",
"git": "Git",
"apiTokens": "API 和權杖",
"tasks": "任務",
"notifications": "通知",
"plugins": "外掛",
"about": "關於"
},
"notifications": {
"title": "通知",
"description": "控制你希望接收的通知事件。",
"webPush": {
"title": "Web 推播通知",
"enable": "啟用推播通知",
"disable": "關閉推播通知",
"enabled": "推播通知已啟用",
"loading": "更新中...",
"unsupported": "此瀏覽器不支援推播通知。",
"denied": "推播通知已被封鎖,請在瀏覽器設定中允許。"
},
"events": {
"title": "事件類型",
"actionRequired": "需要處理",
"stop": "執行已停止",
"error": "執行失敗"
}
},
"appearanceSettings": {
"darkMode": {
"label": "深色模式",
"description": "切換淺色和深色佈景主題"
},
"projectSorting": {
"label": "專案排序",
"description": "專案在側邊欄中的排列方式",
"alphabetical": "依字母順序",
"recentActivity": "最近活動"
},
"codeEditor": {
"title": "程式碼編輯器",
"theme": {
"label": "編輯器佈景主題",
"description": "程式碼編輯器的預設佈景主題"
},
"wordWrap": {
"label": "自動換行",
"description": "在編輯器中預設啟用自動換行"
},
"showMinimap": {
"label": "顯示縮圖",
"description": "在差異檢視中顯示縮圖以便於導覽"
},
"lineNumbers": {
"label": "顯示行號",
"description": "在編輯器中顯示行號"
},
"fontSize": {
"label": "字型大小",
"description": "編輯器字型大小px"
}
}
},
"mcpForm": {
"title": {
"add": "新增 MCP 伺服器",
"edit": "編輯 MCP 伺服器"
},
"importMode": {
"form": "表單輸入",
"json": "JSON 匯入"
},
"scope": {
"label": "範圍",
"userGlobal": "使用者(全域)",
"projectLocal": "專案(本機)",
"userDescription": "使用者範圍:在您機器上的所有專案中可用",
"projectDescription": "本機範圍:僅在選定專案中可用",
"cannotChange": "編輯現有伺服器時無法變更範圍"
},
"fields": {
"serverName": "伺服器名稱",
"transportType": "傳輸類型",
"command": "指令",
"arguments": "參數(每行一個)",
"jsonConfig": "JSON 設定",
"url": "URL",
"envVars": "環境變數KEY=值,每行一個)",
"headers": "標頭KEY=值,每行一個)",
"selectProject": "選取專案..."
},
"placeholders": {
"serverName": "我的服務"
},
"validation": {
"missingType": "缺少必填欄位type",
"stdioRequiresCommand": "stdio 類型需要 command 欄位",
"httpRequiresUrl": "{{type}} 類型需要 url 欄位",
"invalidJson": "無效的 JSON 格式",
"jsonHelp": "貼上您的 MCP 伺服器設定JSON 格式)。範例格式:",
"jsonExampleStdio": "• stdio: {\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"@upstash/context7-mcp\"]}",
"jsonExampleHttp": "• http/sse: {\"type\":\"http\",\"url\":\"https://api.example.com/mcp\"}"
},
"configDetails": "設定詳細資訊(來自 {{configFile}}",
"projectPath": "路徑:{{path}}",
"actions": {
"cancel": "取消",
"saving": "儲存中...",
"addServer": "新增伺服器",
"updateServer": "更新伺服器"
}
},
"saveStatus": {
"success": "設定儲存成功!",
"error": "儲存設定失敗",
"saving": "儲存中..."
},
"footerActions": {
"save": "儲存設定",
"cancel": "取消"
},
"git": {
"title": "Git 設定",
"description": "設定您的 git 提交身分。這些設定將透過 git config --global 全域套用",
"name": {
"label": "Git 名稱",
"help": "您的 git 提交名稱"
},
"email": {
"label": "Git 電子郵件",
"help": "您的 git 提交電子郵件"
},
"actions": {
"save": "儲存設定",
"saving": "儲存中..."
},
"status": {
"success": "儲存成功"
}
},
"apiKeys": {
"title": "API 金鑰",
"description": "產生 API 金鑰以從其他應用程式存取外部 API。",
"newKey": {
"alertTitle": "⚠️ 儲存您的 API 金鑰",
"alertMessage": "這是您唯一一次看到此金鑰。請妥善保存。",
"iveSavedIt": "我已儲存"
},
"form": {
"placeholder": "API 金鑰名稱(例如:正式伺服器)",
"createButton": "建立",
"cancelButton": "取消"
},
"newButton": "新增 API 金鑰",
"empty": "尚未建立 API 金鑰。",
"list": {
"created": "建立時間:",
"lastUsed": "最後使用:"
},
"confirmDelete": "確定要刪除此 API 金鑰嗎?",
"status": {
"active": "啟用",
"inactive": "未啟用"
},
"github": {
"title": "GitHub 權杖",
"description": "新增 GitHub 個人存取權杖以透過外部 API 複製私有儲存庫。",
"descriptionAlt": "新增 GitHub 個人存取權杖以複製私有儲存庫。您也可以直接在 API 請求中傳遞權杖而無需儲存。",
"addButton": "新增權杖",
"form": {
"namePlaceholder": "權杖名稱(例如:個人儲存庫)",
"tokenPlaceholder": "GitHub 個人存取權杖ghp_...",
"descriptionPlaceholder": "描述(選填)",
"addButton": "新增權杖",
"cancelButton": "取消",
"howToCreate": "如何建立 GitHub 個人存取權杖 →"
},
"empty": "尚未新增 GitHub 權杖。",
"added": "新增時間:",
"confirmDelete": "確定要刪除此 GitHub 權杖嗎?"
},
"apiDocsLink": "API 文件",
"documentation": {
"title": "外部 API 文件",
"description": "了解如何使用外部 API 從您的應用程式觸發 Claude/Cursor 工作階段。",
"viewLink": "查看 API 文件 →"
},
"loading": "載入中...",
"version": {
"updateAvailable": "有可用更新v{{version}}"
}
},
"tasks": {
"checking": "正在檢查 TaskMaster 安裝...",
"notInstalled": {
"title": "未安裝 TaskMaster AI CLI",
"description": "需要 TaskMaster CLI 才能使用任務管理功能。安裝它以開始使用:",
"installCommand": "npm install -g task-master-ai",
"viewOnGitHub": "在 GitHub 上查看",
"afterInstallation": "安裝後:",
"steps": {
"restart": "重新啟動此應用程式",
"autoAvailable": "TaskMaster 功能將自動啟用",
"initCommand": "在專案目錄中使用 task-master init"
}
},
"settings": {
"enableLabel": "啟用 TaskMaster 整合",
"enableDescription": "在整個介面中顯示 TaskMaster 任務、橫幅和側邊欄指示器"
}
},
"agents": {
"authStatus": {
"checking": "檢查中...",
"connected": "已連線",
"notConnected": "未連線",
"disconnected": "已中斷連線",
"checkingAuth": "正在檢查驗證狀態...",
"loggedInAs": "登入為 {{email}}",
"authenticatedUser": "已驗證使用者"
},
"account": {
"claude": {
"description": "Anthropic Claude AI 助手"
},
"cursor": {
"description": "Cursor AI 驅動的程式碼編輯器"
},
"codex": {
"description": "OpenAI Codex AI 助手"
},
"gemini": {
"description": "Google Gemini AI 助手"
}
},
"connectionStatus": "連線狀態",
"login": {
"title": "登入",
"reAuthenticate": "重新驗證",
"description": "登入您的 {{agent}} 帳戶以啟用 AI 功能",
"reAuthDescription": "使用其他帳戶登入或重新整理憑證",
"button": "登入",
"reLoginButton": "重新登入"
},
"error": "錯誤:{{error}}"
},
"permissions": {
"title": "權限設定",
"skipPermissions": {
"label": "略過權限提示(請謹慎使用)",
"claudeDescription": "等同於 --dangerously-skip-permissions 旗標",
"cursorDescription": "等同於 Cursor CLI 中的 -f 旗標"
},
"allowedTools": {
"title": "允許的工具",
"description": "無需權限提示即可自動使用的工具",
"placeholder": "例如:\"Bash(git log:*)\" 或 \"Write\"",
"quickAdd": "快速新增常用工具:",
"empty": "未設定允許的工具"
},
"blockedTools": {
"title": "停用的工具",
"description": "無需權限提示即可自動停用的工具",
"placeholder": "例如:\"Bash(rm:*)\"",
"empty": "未設定停用的工具"
},
"allowedCommands": {
"title": "允許的 Shell 指令",
"description": "無需權限提示即可自動執行的 Shell 指令",
"placeholder": "例如:\"Shell(ls)\" 或 \"Shell(git status)\"",
"quickAdd": "快速新增常用指令:",
"empty": "未設定允許的指令"
},
"blockedCommands": {
"title": "封鎖的 Shell 指令",
"description": "自動封鎖的 Shell 指令",
"placeholder": "例如:\"Shell(rm -rf)\" 或 \"Shell(sudo)\"",
"empty": "未設定封鎖的指令"
},
"toolExamples": {
"title": "工具模式範例:",
"bashGitLog": "- 允許所有 git log 指令",
"bashGitDiff": "- 允許所有 git diff 指令",
"write": "- 允許所有 Write 工具使用",
"bashRm": "- 封鎖所有 rm 指令(危險)"
},
"shellExamples": {
"title": "Shell 指令範例:",
"ls": "- 允許 ls 指令",
"gitStatus": "- 允許 git status",
"npmInstall": "- 允許 npm install",
"rmRf": "- 封鎖遞迴刪除"
},
"codex": {
"permissionMode": "權限模式",
"description": "控制 Codex 如何處理檔案修改和指令執行",
"modes": {
"default": {
"title": "預設",
"description": "只有受信任的指令ls、cat、grep、git status 等)會自動執行。其他指令將被略過。可以寫入工作區。"
},
"acceptEdits": {
"title": "接受編輯",
"description": "所有指令在工作區內自動執行。具有沙箱執行的全自動模式。"
},
"bypassPermissions": {
"title": "略過權限",
"description": "完全系統存取,無任何限制。所有指令自動執行,具有完整的磁碟和網路存取權限。請謹慎使用。"
}
},
"technicalDetails": "技術詳情",
"technicalInfo": {
"default": "sandboxMode=workspace-write, approvalPolicy=untrusted。受信任的指令cat、cd、grep、head、ls、pwd、tail、git status/log/diff/show、find不帶 -exec等。",
"acceptEdits": "sandboxMode=workspace-write, approvalPolicy=never。所有指令在專案目錄內自動執行。",
"bypassPermissions": "sandboxMode=danger-full-access, approvalPolicy=never。完全系統存取權限僅在可信環境中使用。",
"overrideNote": "您可以使用聊天介面中的模式按鈕按工作階段覆蓋此設定。"
}
},
"actions": {
"add": "新增"
}
},
"mcpServers": {
"title": "MCP 伺服器",
"description": {
"claude": "Model Context Protocol 伺服器為 Claude 提供額外的工具和資料來源",
"cursor": "Model Context Protocol 伺服器為 Cursor 提供額外的工具和資料來源",
"codex": "Model Context Protocol 伺服器為 Codex 提供額外的工具和資料來源"
},
"addButton": "新增 MCP 伺服器",
"empty": "未設定 MCP 伺服器",
"serverType": "類型",
"scope": {
"local": "本機",
"user": "使用者"
},
"config": {
"command": "指令",
"url": "URL",
"args": "參數",
"environment": "環境變數"
},
"tools": {
"title": "工具",
"count": "{{count}}",
"more": "還有 {{count}} 個"
},
"actions": {
"edit": "編輯伺服器",
"delete": "刪除伺服器"
},
"help": {
"title": "關於 Codex MCP",
"description": "Codex 支援基於 stdio 的 MCP 伺服器。您可以新增伺服器,透過額外的工具和資源來擴充 Codex 的功能。"
}
},
"pluginSettings": {
"title": "外掛",
"description": "透過自訂外掛擴充介面。從 git 安裝或直接將資料夾放入 ~/.claude-code-ui/plugins/",
"installPlaceholder": "https://github.com/user/my-plugin",
"installButton": "安裝",
"installing": "安裝中…",
"securityWarning": "僅安裝您已審查過原始碼或信任作者的外掛。",
"scanningPlugins": "正在掃描外掛…",
"noPluginsInstalled": "未安裝外掛",
"pullLatest": "從 git 拉取最新內容",
"noGitRemote": "無 git 遠端儲存庫 — 無法更新",
"uninstallPlugin": "解除安裝外掛",
"confirmUninstall": "再次點擊確認",
"confirmUninstallMessage": "移除 {{name}}?此操作無法復原。",
"cancel": "取消",
"remove": "移除",
"updateFailed": "更新失敗",
"installFailed": "安裝失敗",
"uninstallFailed": "解除安裝失敗",
"toggleFailed": "切換失敗",
"buildYourOwn": "建立您自己的外掛",
"starter": "入門範本",
"docs": "文件",
"starterPlugin": {
"name": "專案統計",
"badge": "入門",
"description": "查看專案的檔案數、程式碼行數、檔案類型分佈以及最近活動。",
"install": "安裝"
},
"morePlugins": "更多",
"enable": "啟用",
"disable": "停用",
"installAriaLabel": "外掛 git 儲存庫 URL",
"tab": "分頁",
"runningStatus": "執行中"
}
}

View File

@@ -1,136 +0,0 @@
{
"projects": {
"title": "專案",
"newProject": "新增專案",
"deleteProject": "移除專案",
"renameProject": "重新命名專案",
"noProjects": "找不到專案",
"loadingProjects": "載入專案中...",
"searchPlaceholder": "搜尋專案...",
"projectNamePlaceholder": "專案名稱",
"starred": "星號標記",
"all": "全部",
"untitledSession": "未命名工作階段",
"newSession": "新工作階段",
"codexSession": "Codex 工作階段",
"fetchingProjects": "正在取得您的 Claude 專案和工作階段",
"projects": "專案",
"noMatchingProjects": "找不到符合的專案",
"tryDifferentSearch": "嘗試調整您的搜尋詞",
"runClaudeCli": "在專案目錄中執行 Claude CLI 以開始使用"
},
"app": {
"title": "CloudCLI",
"subtitle": "AI 程式開發助手"
},
"sessions": {
"title": "工作階段",
"newSession": "新增工作階段",
"deleteSession": "刪除工作階段",
"renameSession": "重新命名工作階段",
"noSessions": "暫無工作階段",
"loadingSessions": "載入工作階段中...",
"unnamed": "未命名",
"loading": "載入中...",
"showMore": "顯示更多工作階段"
},
"tooltips": {
"viewEnvironments": "查看環境",
"hideSidebar": "隱藏側邊欄",
"createProject": "建立新專案",
"refresh": "重新整理專案和工作階段 (Ctrl+R)",
"renameProject": "重新命名專案 (F2)",
"deleteProject": "從側邊欄移除專案 (Delete)",
"addToFavorites": "加入收藏",
"removeFromFavorites": "從收藏移除",
"editSessionName": "手動編輯工作階段名稱",
"deleteSession": "永久刪除此工作階段",
"save": "儲存",
"cancel": "取消",
"clearSearch": "清除搜尋",
"openCommandPalette": "開啟指令面板"
},
"navigation": {
"chat": "聊天",
"files": "檔案",
"git": "Git",
"terminal": "終端機",
"tasks": "任務"
},
"actions": {
"refresh": "重新整理",
"settings": "設定",
"collapseAll": "全部收合",
"expandAll": "全部展開",
"cancel": "取消",
"save": "儲存",
"delete": "刪除",
"rename": "重新命名",
"joinCommunity": "加入社群",
"reportIssue": "回報問題",
"starOnGithub": "在 GitHub 上加星"
},
"branding": {
"openSource": "開源"
},
"status": {
"active": "使用中",
"inactive": "非使用中",
"thinking": "思考中...",
"error": "錯誤",
"aborted": "已中止",
"unknown": "未知"
},
"time": {
"justNow": "剛剛",
"oneMinuteAgo": "1 分鐘前",
"minutesAgo": "{{count}} 分鐘前",
"oneHourAgo": "1 小時前",
"hoursAgo": "{{count}} 小時前",
"oneDayAgo": "1 天前",
"daysAgo": "{{count}} 天前"
},
"messages": {
"deleteConfirm": "確定要刪除嗎?",
"renameSuccess": "重新命名成功",
"deleteSuccess": "刪除成功",
"errorOccurred": "發生錯誤",
"deleteSessionConfirm": "確定要刪除此工作階段嗎?此操作無法復原。",
"deleteProjectConfirm": "從側邊欄移除此專案?您的專案檔案、記憶和工作階段資料不會被刪除。",
"enterProjectPath": "請輸入專案路徑",
"deleteSessionFailed": "刪除工作階段失敗,請重試。",
"deleteSessionError": "刪除工作階段時出錯,請重試。",
"renameSessionFailed": "重新命名工作階段失敗,請重試。",
"renameSessionError": "重新命名工作階段時出錯,請重試。",
"deleteProjectFailed": "移除專案失敗,請重試。",
"deleteProjectError": "移除專案時出錯,請重試。",
"createProjectFailed": "建立專案失敗,請重試。",
"createProjectError": "建立專案時出錯,請重試。"
},
"version": {
"updateAvailable": "有可用更新"
},
"search": {
"modeProjects": "專案",
"modeConversations": "對話",
"conversationsPlaceholder": "搜尋對話內容...",
"searching": "搜尋中...",
"noResults": "找不到結果",
"tryDifferentQuery": "嘗試不同的搜尋詞",
"matches_one": "{{count}} 個符合",
"matches_other": "{{count}} 個符合",
"projectsScanned_one": "{{count}} 個專案已掃描",
"projectsScanned_other": "{{count}} 個專案已掃描"
},
"deleteConfirmation": {
"deleteProject": "移除專案",
"deleteSession": "刪除工作階段",
"confirmDelete": "您想如何處理",
"sessionCount_one": "此專案包含 {{count}} 個對話。",
"sessionCount_other": "此專案包含 {{count}} 個對話。",
"removeFromSidebar": "僅從側邊欄移除",
"deleteAllData": "永久刪除所有資料",
"allConversationsDeleted": "專案將從側邊欄中移除。您的檔案、記憶和工作階段資料將會保留。",
"cannotUndo": "您可以稍後重新新增此專案。"
}
}

View File

@@ -1,142 +0,0 @@
{
"notConfigured": {
"title": "TaskMaster AI 尚未設定",
"description": "TaskMaster 協助將複雜的專案分解為可管理的任務,搭配 AI 驅動的輔助功能",
"whatIsTitle": "🎯 什麼是 TaskMaster",
"features": {
"aiPowered": "AI 驅動的任務管理:將複雜專案分解為可管理的子任務",
"prdTemplates": "PRD 範本:從產品需求文件產生任務",
"dependencyTracking": "相依性追蹤:了解任務關係和執行順序",
"progressVisualization": "進度視覺化:看板和詳細的任務分析",
"cliIntegration": "CLI 整合:使用 taskmaster 指令進行進階工作流程"
},
"initializeButton": "初始化 TaskMaster AI"
},
"gettingStarted": {
"title": "開始使用 TaskMaster",
"subtitle": "TaskMaster 已初始化!以下是接下來要做的事:",
"steps": {
"createPRD": {
"title": "建立產品需求文件PRD",
"description": "討論您的專案構想並建立描述您想建立什麼的 PRD。",
"addButton": "新增 PRD",
"existingPRDs": "現有的 PRD"
},
"generateTasks": {
"title": "從 PRD 產生任務",
"description": "一旦您有了 PRD請 AI 助手解析它TaskMaster 將自動將其分解為可管理的任務,包含實作細節。"
},
"analyzeTasks": {
"title": "分析並展開任務",
"description": "請 AI 助手分析任務複雜度,並將其展開為詳細的子任務以便於實作。"
},
"startBuilding": {
"title": "開始建構",
"description": "請 AI 助手開始處理任務、更新狀態,並在專案演進時新增任務。"
}
},
"tip": "💡 提示:從 PRD 開始可以充分利用 TaskMaster 的 AI 驅動任務產生功能"
},
"setupModal": {
"title": "TaskMaster 設定",
"subtitle": "{{projectName}} 的互動式 CLI",
"willStart": "TaskMaster 初始化將自動開始",
"completed": "TaskMaster 設定完成!您現在可以關閉此視窗。",
"closeButton": "關閉",
"closeContinueButton": "關閉並繼續"
},
"helpGuide": {
"title": "開始使用 TaskMaster",
"subtitle": "您的高效任務管理指南",
"examples": {
"parsePRD": "💬 範例:\n「我剛用 Claude Task Master 初始化了一個新專案。我有一個 PRD 在 .taskmaster/docs/prd.txt。你能幫我解析它並設定初始任務嗎」",
"expandTask": "💬 範例:\n「任務 5 看起來很複雜。你能把它分解成子任務嗎?」",
"addTask": "💬 範例:\n「請新增一個任務來實作使用 Cloudinary 的使用者個人頭像上傳功能,研究最佳方法。」"
},
"moreExamples": "查看更多範例和使用模式 →",
"proTips": {
"title": "💡 專業提示",
"search": "使用搜尋列快速找到特定任務",
"views": "使用檢視切換在看板、清單和網格檢視之間切換",
"filters": "使用篩選器聚焦特定任務狀態或優先順序",
"details": "點擊任何任務以查看詳細資訊和管理子任務"
},
"learnMore": {
"title": "📚 了解更多",
"description": "TaskMaster AI 是為開發者打造的進階任務管理系統。取得文件、範例並為專案做出貢獻。",
"githubButton": "在 GitHub 上查看"
}
},
"search": {
"placeholder": "搜尋任務..."
},
"filters": {
"button": "篩選",
"status": "狀態",
"priority": "優先順序",
"sortBy": "排序依據",
"allStatuses": "所有狀態",
"allPriorities": "所有優先順序",
"showing": "顯示 {{filtered}} / {{total}} 個任務",
"clearFilters": "清除篩選"
},
"sort": {
"id": "ID",
"status": "狀態",
"priority": "優先順序",
"idAsc": "ID遞增",
"idDesc": "ID遞減",
"titleAsc": "標題A-Z",
"titleDesc": "標題Z-A",
"statusAsc": "狀態(待處理優先)",
"statusDesc": "狀態(已完成優先)",
"priorityAsc": "優先順序(高優先)",
"priorityDesc": "優先順序(低優先)"
},
"views": {
"kanban": "看板檢視",
"list": "清單檢視",
"grid": "網格檢視"
},
"kanban": {
"pending": "📋 待辦",
"inProgress": "🚀 進行中",
"done": "✅ 已完成",
"blocked": "🚫 已封鎖",
"deferred": "⏳ 已延後",
"cancelled": "❌ 已取消",
"noTasksYet": "尚無任務",
"tasksWillAppear": "任務將顯示在這裡",
"moveTasksHere": "開始後將任務移到這裡",
"completedTasksHere": "已完成的任務顯示在這裡",
"statusTasksHere": "此狀態的任務將顯示在這裡"
},
"buttons": {
"help": "TaskMaster 入門指南",
"prds": "PRD",
"addPRD": "新增 PRD",
"addTask": "新增任務",
"createNewPRD": "建立新 PRD",
"prdsAvailable": "{{count}} 個 PRD 可用"
},
"prd": {
"modified": "修改時間:{{date}}"
},
"statuses": {
"pending": "待處理",
"in-progress": "進行中",
"done": "已完成",
"blocked": "已封鎖",
"deferred": "已延後",
"cancelled": "已取消"
},
"priorities": {
"high": "高",
"medium": "中",
"low": "低"
},
"noMatchingTasks": {
"title": "沒有符合篩選條件的任務",
"description": "嘗試調整您的搜尋或篩選條件。"
}
}