Compare commits

...

22 Commits

Author SHA1 Message Date
Simos Mikelatos
bc9d2dd830 Merge pull request #839 from siteboon/fix/claude-token-cache-usage 2026-06-05 21:48:44 +02:00
妖怪不丸
c21a9f4561 feat(i18n): add Traditional Chinese (zh-TW) locale (#773)
* feat(i18n): add Traditional Chinese (zh-TW) locale

* fix(i18n): localize remaining English strings and fix README fence (zh-TW)

* fix(i18n): track zh-TW tasks.json (was excluded by .gitignore)

---------

Co-authored-by: Haile <118998054+blackmammoth@users.noreply.github.com>
2026-06-05 21:45:49 +03:00
Haileyesus
ed9cdf0114 fix: include Claude cache tokens in usage 2026-06-05 21:38:05 +03:00
Simos Mikelatos
b39997c429 Merge pull request #838 from siteboon/fix/do-not-show-model-description-in-chat-view 2026-06-05 20:31:55 +02:00
Haileyesus
d638a8982c fix: do not show model description in chat view 2026-06-05 21:28:08 +03:00
Simos Mikelatos
f238050b85 feat(chat): open cost modal from token usage 2026-06-05 17:33:22 +00:00
viper151
beaa2d2533 chore(release): v1.33.1 2026-06-05 16:56:27 +00:00
Simos Mikelatos
c90b34108e chore: update package-lock.json 2026-06-05 16:54:22 +00:00
Simos Mikelatos
323357384e Merge pull request #837 from siteboon/fix/tool-result-error-rendering 2026-06-05 17:40:54 +02:00
Simos Mikelatos
d509aa635b Merge pull request #834 from siteboon/chore/update-claude-fallback-models 2026-06-05 17:36:22 +02:00
Haile
2149b8776b fix: remove thinking mode (#835) 2026-06-05 17:35:39 +02:00
Haile
2b416f2dcb Merge branch 'main' into fix/tool-result-error-rendering 2026-06-05 16:32:51 +03:00
Haileyesus
bb8db5815c fix: show Claude tool result errors
Claude stores some tool failures as errored tool_result rows. The UI either
attached those rows to hidden tool output or dropped them when no matching tool
call was rendered, which made validation failures disappear from chat history.

Render unattached errored tool results, unwrap Claude tool_use_error content,
and keep tool errors visible even for tools whose successful output is hidden.
Also remove the permission-grant recovery controls from rendered error history
so denied tool use stays a plain error message.
2026-06-05 16:16:34 +03:00
Haile
b3d0f9037d Merge branch 'main' into chore/update-claude-fallback-models 2026-06-05 15:58:05 +03:00
Haile
3ec76b5bb1 docs: add nginx subpath deployment template (#820)
Users deploying behind a reverse proxy need a config they can adapt.

The template documents each proxy block and centralizes upstream/subpath values.

It also notes that Nginx location matchers still require literal subpath edits.
2026-06-05 14:24:26 +02:00
Haile
14ddbc7c57 fix: redact websocket auth token in logs (#827) 2026-06-05 14:23:27 +02:00
Haile
ebb0e59e80 fix: file tree concurrency (#828)
* perf(file-tree): parallelize directory traversal and widen default ignore list

The project file-tree endpoint walked children sequentially with
`await fsPromises.stat()` inside a for-loop plus a separate
`fsPromises.access()` probe before recursing. On high-latency
filesystems (NFS/SMB) every one of those round-trips was serialized,
so a 120k-file SMB-mounted project took ~2 minutes to load.

This change:
* Runs stat() and recursive getFileTree() calls in parallel via
  `Promise.all` — pipelines round-trips and lets subtree traversals
  overlap.
* Drops the redundant access() probe; any EACCES now surfaces from
  readdir's own try/catch in the recursive call, saving one RTT per
  directory.
* Extracts the hardcoded skip list into an IGNORED_DIRS Set and
  extends it to cover common Python / Rust / JVM / IDE build
  artefacts (.next, __pycache__, .pytest_cache, .tox, .venv,
  target, .gradle, .idea, coverage, etc).

No API shape change; existing consumers get the same tree structure,
only much faster on large or remote-mounted projects.

* fix(file-tree): bound filesystem traversal concurrency

Prevent large file-tree scans from launching unbounded stat and readdir work.

Keep the parallel traversal benefit on high-latency mounts with a bounded queue.

Ignore skipped names only for directories so same-named files stay visible.

* fix(file-tree): inspect entries with lstat

Use lstat for file-tree metadata so symlink entries are identified without following targets.

---------

Co-authored-by: leonkong via Claude <leonkong.claude@users.noreply.github.com>
2026-06-05 14:21:30 +02:00
Haile
957f53fb99 Merge branch 'main' into chore/update-claude-fallback-models 2026-06-05 15:19:13 +03:00
Haile
ef2fd48b46 fix(shell): disconnect and restart buttons (#831)
Co-authored-by: Simos Mikelatos <simosmik@gmail.com>
2026-06-05 14:17:20 +02:00
Haileyesus
cdcac182d4 fix: load claude models directly from provider
Claude's model catalog changes quickly enough that a shared three-day cache can
leave users selecting stale defaults or missing newly available model aliases.
Route Claude model lookups through the provider every time so the UI and slash
commands reflect the current provider result instead of an old disk snapshot.

Keep the static fallback catalog aligned with the latest Claude defaults so the
provider still has a sensible response when live discovery is unavailable.
2026-06-05 15:14:32 +03:00
Haileyesus
94785bfa57 chore: update Claude fallback models 2026-06-05 15:02:25 +03:00
Haile
9e608b8426 Fixes/minor fixes (#832)
* chore: update claude agent sdk to latest version

* fix: show CTRL+K correctly in chatview
2026-06-05 13:56:34 +02:00
54 changed files with 2500 additions and 891 deletions

View File

@@ -3,6 +3,35 @@
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)
### 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>
</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.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.zh-TW.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>
</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.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> · <a href="./README.zh-TW.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>
</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.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.zh-TW.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>
</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.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.zh-TW.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>
</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.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.zh-TW.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>
</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.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.zh-TW.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>
</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.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.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.tr.md">Türkçe</a></i></div>
---

242
README.zh-TW.md Normal file
View File

@@ -0,0 +1,242 @@
<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

@@ -0,0 +1,218 @@
# 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;
}
}
}

244
package-lock.json generated
View File

@@ -1,16 +1,16 @@
{
"name": "@cloudcli-ai/cloudcli",
"version": "1.33.0",
"version": "1.33.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@cloudcli-ai/cloudcli",
"version": "1.33.0",
"version": "1.33.1",
"hasInstallScript": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.116",
"@anthropic-ai/claude-agent-sdk": "^0.3.165",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",
@@ -139,35 +139,33 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.2.116",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.116.tgz",
"integrity": "sha512-5NKpgaOZkzNCGCvLxJZUVGimf5IcYmpQ2x2XrR9ilK+2UkWrnnwcUfIWo8bBz9e7lSYcUf9XleGigq2eOOF7aw==",
"version": "0.3.165",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.165.tgz",
"integrity": "sha512-wEUJNTAWkE6KMV35abqGi30lwhZz+jQLMtLh4SuTN2Hllzsysq8kmQFgcWulza3FLHG/GHzGHPi0+Sp2fb8xlw==",
"license": "SEE LICENSE IN README.md",
"dependencies": {
"@anthropic-ai/sdk": "^0.81.0",
"@modelcontextprotocol/sdk": "^1.29.0"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.116",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.116",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.116",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.116",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.116",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.116",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.116",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.116"
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.165",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.165",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.165",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.165",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.165",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.165",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.165",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.165"
},
"peerDependencies": {
"@anthropic-ai/sdk": ">=0.93.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"zod": "^4.0.0"
}
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
"version": "0.2.116",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.2.116.tgz",
"integrity": "sha512-mG19ovtXCpETmd5KmTU1JO2iIHZBG09IP8DmgZjLA3wLmTzpgn9Au9veRaeJeXb1EqiHiFZU+z+mNB79+w5v9g==",
"version": "0.3.165",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.165.tgz",
"integrity": "sha512-obVodJmppNc6lgcM6Y5y3VCQLrYO2curOXrRaziKtjxYbuZP7kYsUhnonMvGoVAQh3uHKz2tivQDeztvWe3f9w==",
"cpu": [
"arm64"
],
@@ -178,9 +176,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
"version": "0.2.116",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.2.116.tgz",
"integrity": "sha512-qC25N0HRM8IXbM4Qi4svH9f51Y6DciDvjLV+oNYnxkdPgDG8p/+b7vQirN7qPxytIQb2TPdoFgUeCsSe7lrQyw==",
"version": "0.3.165",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.165.tgz",
"integrity": "sha512-0jc1tlYLXzPvZIkHKGHzsEEKq2YqTS8oHSNFroqLgbhrIk1Zy05ZXbciI289VDAe1Fq2a+qcUhkXct8Parx1Rg==",
"cpu": [
"x64"
],
@@ -191,9 +189,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
"version": "0.2.116",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.2.116.tgz",
"integrity": "sha512-MQIcJhhPM+RPJ7kMQdOQarkJ2FlJqOiu953c08YyJOoWdHykd3DIiHws3mf1Mwl/dfFeIyshOVpNND3hyIy5Dg==",
"version": "0.3.165",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.165.tgz",
"integrity": "sha512-t87HgDPPaRYMTTB5cqA0M36Fyq4DOny89yk71BMgA8hAzhOjV9bla8pMVZTuX3xYYPjsa/TOmxSzwI8GZLf4Aw==",
"cpu": [
"arm64"
],
@@ -204,9 +202,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
"version": "0.2.116",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.2.116.tgz",
"integrity": "sha512-Dg/T3NkSp35ODiwdhj0KquvC6Xu+DMbyWFNkfepA3bz4oF2SVSgyOPYwVmfoJerzEUnYDldP4YhOxRrhbt0vXA==",
"version": "0.3.165",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.165.tgz",
"integrity": "sha512-Rccmr5chZdZJVRvoB0nildB5PTKX+amatUho9JIcNOf1iX/6ej39fwf8q9W1MRHYP7AEc4t9GrSAGLcn7/JO4w==",
"cpu": [
"arm64"
],
@@ -217,9 +215,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
"version": "0.2.116",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.2.116.tgz",
"integrity": "sha512-Bww1fzQB+vcF0tRhmCAlwSsN4wR2HgX7pBT9AWuwzJj6DKsVC23N54Ea80lsnM7dTUtUTrGYMTwVUHTWqfYnfQ==",
"version": "0.3.165",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.165.tgz",
"integrity": "sha512-Y8fEW0zKBn0XZI5AOQWHep0Srz0qsCauynTWkhsC6J2vSPxkTiOxv2hmb7qdfiNlFn0k1etCWVFoRkhhFJzGfg==",
"cpu": [
"x64"
],
@@ -230,9 +228,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
"version": "0.2.116",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.2.116.tgz",
"integrity": "sha512-LMYxUMa1nK4N9BPRJdcGBAvl9rjTI4ZHo+kfAKrJ3MlfB6VFF1tRIubwsWOaOtkuNazMdAYovsZJg4bdzOBBTQ==",
"version": "0.3.165",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.165.tgz",
"integrity": "sha512-Y9Acr1RmydfEX+t+3mFn0K9VOx6nfyo08QuQH9R6ap1YYZWuobze++pNUY/rzwbQjXqcbjORtPKbO/kLQtSr9w==",
"cpu": [
"x64"
],
@@ -243,9 +241,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
"version": "0.2.116",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.2.116.tgz",
"integrity": "sha512-h0YO1vkTIeUtffQhONrYbNC1pXmk1yjb1xxMEw7bAwucqtFoFpLDWe+q4+RhxaQr8ZOj6LtRE/U3dzPWHOlshA==",
"version": "0.3.165",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.165.tgz",
"integrity": "sha512-4Q01L3xaDDCvlOhABf2MnO7v7yJxKwwDyiMr+DaneUSvuh1qH0YE7qErSYLf6D9VfH8TdRwKZXwQplVVwCoHWw==",
"cpu": [
"arm64"
],
@@ -256,9 +254,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
"version": "0.2.116",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.2.116.tgz",
"integrity": "sha512-3lllmtDFHgpW0ZM3iNvxsEjblrgRzF9Qm1lxTOtunP3hIn+pA/IkWMtKlN1ixxWiaBguLVQkJ90V6JHsvJJIvw==",
"version": "0.3.165",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.165.tgz",
"integrity": "sha512-Y0uOx7b7ZnkguvFFI5T5fSLnRA/e0uvMC++gSnyz6XMpNekgWc3+Mny7Dv2NO22nKbV2YiFsj6MkYYFEd51BDw==",
"cpu": [
"x64"
],
@@ -269,12 +267,14 @@
]
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.81.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.81.0.tgz",
"integrity": "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw==",
"version": "0.100.1",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.100.1.tgz",
"integrity": "sha512-RANcEe7LpiLczkKGOwoXOTuFdPhuubS0i4xaAKOMpcqc55YO0mukgxppV7eygx3DXNjxWT6RYOLPyOy0aIAmwg==",
"license": "MIT",
"peer": true,
"dependencies": {
"json-schema-to-ts": "^3.1.1"
"json-schema-to-ts": "^3.1.1",
"standardwebhooks": "^1.0.0"
},
"bin": {
"anthropic-ai-sdk": "bin/cli"
@@ -1837,6 +1837,7 @@
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
"integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.14.1"
},
@@ -2601,6 +2602,7 @@
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
"integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@hono/node-server": "^1.19.9",
"ajv": "^8.17.1",
@@ -2641,6 +2643,7 @@
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"license": "MIT",
"peer": true,
"dependencies": {
"mime-types": "^3.0.0",
"negotiator": "^1.0.0"
@@ -2650,10 +2653,11 @@
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -2670,6 +2674,7 @@
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
"license": "MIT",
"peer": true,
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
@@ -2694,6 +2699,7 @@
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -2707,6 +2713,7 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.6.0"
}
@@ -2716,6 +2723,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -2759,6 +2767,7 @@
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
"license": "MIT",
"peer": true,
"dependencies": {
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
@@ -2780,6 +2789,7 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8"
}
@@ -2789,6 +2799,7 @@
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
@@ -2809,6 +2820,7 @@
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT",
"peer": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
@@ -2824,13 +2836,15 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8"
}
@@ -2840,6 +2854,7 @@
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -2852,6 +2867,7 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"license": "MIT",
"peer": true,
"dependencies": {
"mime-db": "^1.54.0"
},
@@ -2868,15 +2884,17 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/qs": {
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"side-channel": "^1.1.0"
},
@@ -2892,6 +2910,7 @@
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"license": "MIT",
"peer": true,
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
@@ -2907,6 +2926,7 @@
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"debug": "^4.4.3",
"encodeurl": "^2.0.0",
@@ -2933,6 +2953,7 @@
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
"license": "MIT",
"peer": true,
"dependencies": {
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
@@ -2952,22 +2973,42 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
"license": "MIT",
"peer": true,
"dependencies": {
"content-type": "^1.0.5",
"content-type": "^2.0.0",
"media-typer": "^1.1.0",
"mime-types": "^3.0.0"
},
"engines": {
"node": ">= 0.6"
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/@napi-rs/wasm-runtime": {
@@ -4284,6 +4325,13 @@
"url": "https://ko-fi.com/dangreen"
}
},
"node_modules/@stablelib/base64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
"license": "MIT",
"peer": true
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
@@ -5417,6 +5465,7 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"ajv": "^8.0.0"
},
@@ -5430,10 +5479,11 @@
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -5449,7 +5499,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/ansi-escapes": {
"version": "7.3.0",
@@ -8515,6 +8566,7 @@
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
"license": "MIT",
"peer": true,
"dependencies": {
"eventsource-parser": "^3.0.1"
},
@@ -8523,10 +8575,11 @@
}
},
"node_modules/eventsource-parser": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
"integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz",
"integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
}
@@ -8634,12 +8687,13 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
"license": "MIT",
"peer": true,
"dependencies": {
"ip-address": "10.1.0"
"ip-address": "^10.2.0"
},
"engines": {
"node": ">= 16"
@@ -8755,6 +8809,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-sha256": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
"license": "Unlicense",
"peer": true
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
@@ -9812,10 +9873,11 @@
"license": "CC0-1.0"
},
"node_modules/hono": {
"version": "4.12.14",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
"version": "4.12.23",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz",
"integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -10187,9 +10249,9 @@
}
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -10632,7 +10694,8 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/is-regex": {
"version": "1.2.1",
@@ -10903,10 +10966,11 @@
}
},
"node_modules/jose": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
"integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/panva"
}
@@ -10968,6 +11032,7 @@
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"ts-algebra": "^2.0.0"
@@ -10987,7 +11052,8 @@
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
"license": "BSD-2-Clause"
"license": "BSD-2-Clause",
"peer": true
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@@ -13979,6 +14045,7 @@
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
"integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.20.0"
}
@@ -15279,6 +15346,7 @@
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"debug": "^4.4.0",
"depd": "^2.0.0",
@@ -15295,6 +15363,7 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
@@ -16353,6 +16422,17 @@
"node": ">=12.0.0"
}
},
"node_modules/standardwebhooks": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@stablelib/base64": "^1.0.0",
"fast-sha256": "^1.3.0"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -17063,7 +17143,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/ts-api-utils": {
"version": "2.4.0",
@@ -18949,6 +19030,7 @@
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
"integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
"license": "ISC",
"peer": true,
"peerDependencies": {
"zod": "^3.25.28 || ^4"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@cloudcli-ai/cloudcli",
"version": "1.33.0",
"version": "1.33.1",
"description": "A web-based UI for Claude Code CLI",
"type": "module",
"main": "dist-server/server/index.js",
@@ -67,7 +67,7 @@
"author": "CloudCLI UI Contributors",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.116",
"@anthropic-ai/claude-agent-sdk": "^0.3.165",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",

View File

@@ -11,7 +11,8 @@ export const CLAUDE_MODELS = {
{
value: "default",
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.8 (1M context)) · $5/$25 per Mtok",
},
{
value: "sonnet",
@@ -23,6 +24,12 @@ export const CLAUDE_MODELS = {
label: "Sonnet (1M context)",
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",
label: "Haiku",

View File

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

View File

@@ -87,6 +87,11 @@ const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
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 server = http.createServer(app);
@@ -1386,6 +1391,8 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
let inputTokens = 0;
let outputTokens = 0;
let cacheReadTokens = 0;
let cacheCreationTokens = 0;
// Find the latest assistant message with usage data (scan from end)
for (let i = lines.length - 1; i >= 0; i--) {
@@ -1397,8 +1404,11 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
const usage = entry.message.usage;
// Use token counts from latest assistant message only
inputTokens = usage.input_tokens || 0;
outputTokens = usage.output_tokens || 0;
const directInputTokens = readUsageNumber(usage.input_tokens ?? usage.inputTokens);
cacheReadTokens = readUsageNumber(usage.cache_read_input_tokens ?? usage.cacheReadInputTokens ?? usage.cacheReadTokens);
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
}
@@ -1409,12 +1419,16 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
}
const totalUsed = inputTokens + outputTokens;
const cacheTokens = cacheReadTokens + cacheCreationTokens;
res.json({
used: totalUsed,
total: contextWindow,
inputTokens,
outputTokens,
cacheReadTokens,
cacheCreationTokens,
cacheTokens,
breakdown: {
input: inputTokens,
output: outputTokens
@@ -1483,74 +1497,133 @@ function permToRwx(perm) {
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) {
// Using fsPromises from import
const items = [];
let entries;
try {
const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
// Debug: log all entries including hidden files
// 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);
await acquire();
try {
entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
} finally {
release();
}
} catch (error) {
// Only log non-permission errors to avoid spam
if (error.code !== 'EACCES' && error.code !== 'EPERM') {
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) => {
if (a.type !== b.type) {
return a.type === 'directory' ? -1 : 1;

View File

@@ -18,18 +18,23 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
{
value: 'default',
label: 'Default (recommended)',
description: 'Use the default model (currently Opus 4.7 (1M context)) · $5/$25 per Mtok',
description: 'Use the default model (currently Opus 4.8 (1M context)) · $5/$25 per Mtok',
},
{
value: 'sonnet',
label: 'Sonnet',
description: 'Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok',
value: "sonnet",
label: "Sonnet",
description: "Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok",
},
{
value: 'sonnet[1m]',
label: 'Sonnet (1M context)',
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',
label: 'Haiku',

View File

@@ -17,6 +17,7 @@ import { readProviderSessionActiveModelChange } from '@/shared/utils.js';
export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000;
const PROVIDER_MODELS_CACHE_VERSION = 1;
const UNCACHED_PROVIDERS = new Set<LLMProvider>(['claude']);
type ProviderModelsServiceDependencies = {
resolveProvider?: (provider: LLMProvider) => Pick<IProvider, 'models'>;
@@ -232,10 +233,42 @@ export const createProviderModelsService = (dependencies: ProviderModelsServiceD
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 (
provider: LLMProvider,
options: ProviderModelsOptions = {},
): Promise<ProviderModelsResult> => {
if (UNCACHED_PROVIDERS.has(provider)) {
const pendingRequest = pendingRequests.get(provider);
if (pendingRequest) {
return pendingRequest;
}
return loadDirectModels(provider);
}
if (options.bypassCache) {
const pendingRequest = pendingRequests.get(provider);
if (pendingRequest) {

View File

@@ -130,6 +130,37 @@ 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 () => {
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-cache-file-'));
const cachePath = path.join(tempRoot, 'models-cache.json');

View File

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

View File

@@ -20,7 +20,13 @@ export function verifyWebSocketClient(
dependencies: WebSocketAuthDependencies
): boolean {
const request = info.req as AuthenticatedWebSocketRequest;
console.log('WebSocket connection attempt to:', request.url);
const upgradeUrl = new URL(request.url ?? '/', 'http://localhost');
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.
if (dependencies.isPlatform) {
@@ -36,7 +42,6 @@ export function verifyWebSocketClient(
}
// OSS mode: read JWT from query string first, then Authorization header.
const upgradeUrl = new URL(request.url ?? '/', 'http://localhost');
const token =
upgradeUrl.searchParams.get('token') ??
request.headers.authorization?.split(' ')[1] ??

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,12 @@ import type { NormalizedMessage } from '../../../stores/useSessionStore';
import type { ChatMessage, SubagentChildTool } from '../types/types';
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[]
* that the existing UI components expect.
@@ -20,7 +26,12 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
// First pass: collect tool results for attachment
const toolResultMap = new Map<string, NormalizedMessage>();
const toolUseIds = new Set<string>();
for (const msg of messages) {
if (msg.kind === 'tool_use' && msg.toolId) {
toolUseIds.add(msg.toolId);
}
if (msg.kind === 'tool_result' && msg.toolId) {
toolResultMap.set(msg.toolId, msg);
}
@@ -97,7 +108,7 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
const toolResult = tr
? {
content: typeof tr.content === 'string' ? tr.content : JSON.stringify(tr.content),
content: formatToolResultContent(tr.content),
isError: Boolean(tr.isError),
toolUseResult: (tr as any).toolUseResult,
}
@@ -191,8 +202,25 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
break;
// 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;
}
default:
break;

View File

@@ -564,11 +564,15 @@ export function shouldHideToolResult(toolName: string, toolResult: any): boolean
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
if (config.result.hidden) return true;
// Hide on success only
if (config.result.hideOnSuccess && toolResult && !toolResult.isError) {
if (config.result.hideOnSuccess && toolResult) {
return true;
}

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import { memo, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
import type {
ChatMessage,
@@ -8,10 +9,10 @@ import type {
Provider,
} from '../../types/types';
import { formatUsageLimitText } from '../../utils/chatFormatting';
import { getClaudePermissionSuggestion } from '../../utils/chatPermissions';
import type { Project } from '../../../../types/app';
import { ToolRenderer, shouldHideToolResult } from '../../tools';
import { Reasoning, ReasoningTrigger, ReasoningContent } from '../../../../shared/view/ui';
import { Markdown } from './Markdown';
import MessageCopyControl from './MessageCopyControl';
@@ -41,10 +42,9 @@ type InteractiveOption = {
isSelected: boolean;
};
type PermissionGrantState = 'idle' | 'granted' | 'error';
const COPY_HIDDEN_TOOL_NAMES = new Set(['Bash', 'Edit', 'Write', 'ApplyPatch']);
const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, onShowSettings, onGrantToolPermission, autoExpandTools, showRawParameters, showThinking, selectedProject, provider }: MessageComponentProps) => {
const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, autoExpandTools, showRawParameters, showThinking, selectedProject, provider }: MessageComponentProps) => {
const { t } = useTranslation('chat');
const isGrouped = prevMessage && prevMessage.type === message.type &&
((prevMessage.type === 'assistant') ||
@@ -53,8 +53,6 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
(prevMessage.type === 'error'));
const messageRef = useRef<HTMLDivElement | null>(null);
const [isExpanded, setIsExpanded] = useState(false);
const permissionSuggestion = getClaudePermissionSuggestion(message, provider);
const [permissionGrantState, setPermissionGrantState] = useState<PermissionGrantState>('idle');
const userCopyContent = String(message.content || '');
const formattedMessageContent = useMemo(
() => formatUsageLimitText(String(message.content || '')),
@@ -73,10 +71,6 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
!message.isThinking;
useEffect(() => {
setPermissionGrantState('idle');
}, [permissionSuggestion?.entry, message.toolId]);
useEffect(() => {
const node = messageRef.current;
if (!autoExpandTools || !node || !message.isToolUse) return;
@@ -241,55 +235,6 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
<Markdown className="prose prose-sm prose-red max-w-none dark:prose-invert">
{String(message.toolResult.content || '')}
</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>
) : (

View File

@@ -277,11 +277,15 @@ export default function ProviderSelectionEmptyState({
>
<div className="min-w-0 flex-1">
<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">
{model.description}
</div>
)}
)} */}
</div>
{isSelected && (
<Check className="ml-auto h-4 w-4 shrink-0 text-primary" />
@@ -321,6 +325,7 @@ export default function ProviderSelectionEmptyState({
<p className="mt-3 flex items-center justify-center gap-1.5 text-center text-xs text-muted-foreground/60">
<Trans
ns="chat"
i18nKey="providerSelection.pressToSearch"
values={{ shortcut: MOD_KEY === "⌘" ? "⌘K" : "Ctrl+K" }}
components={{

View File

@@ -1,244 +0,0 @@
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,6 +2,7 @@ import { ActivityIcon } from 'lucide-react';
type TokenUsageSummaryProps = {
usage: Record<string, unknown> | null;
onClick?: () => void;
};
const formatTokenCount = (value: number) => {
@@ -29,7 +30,7 @@ const readUsageNumber = (value: unknown) => {
return Number.isFinite(parsed) ? parsed : 0;
};
export default function TokenUsageSummary({ usage }: TokenUsageSummaryProps) {
export default function TokenUsageSummary({ usage, onClick }: TokenUsageSummaryProps) {
const breakdown =
usage?.breakdown && typeof usage.breakdown === 'object'
? usage.breakdown as Record<string, unknown>
@@ -39,15 +40,18 @@ export default function TokenUsageSummary({ usage }: TokenUsageSummaryProps) {
const usedTokens = readUsageNumber(usage?.used) || inputTokens + outputTokens;
return (
<div
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"
<button
type="button"
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`}
aria-label="Show token usage"
>
<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" />
</span>
<span className="font-medium text-foreground">{formatTokenCount(usedTokens)}</span>
<span className="hidden text-muted-foreground/70 sm:inline">tokens</span>
</div>
</button>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
import { RotateCcw, X } from 'lucide-react';
type ShellHeaderProps = {
isConnected: boolean;
isInitialized: boolean;
@@ -50,34 +52,27 @@ export default function ShellHeader({
{isRestarting && <span className="text-xs text-blue-400">{statusRestartingText}</span>}
</div>
<div className="flex items-center space-x-3">
<div className="flex items-center gap-2">
{isConnected && (
<button
type="button"
onClick={onDisconnect}
className="flex items-center space-x-1 rounded bg-red-600 px-3 py-1 text-xs text-white hover:bg-red-700"
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"
title={disconnectTitle}
>
<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>
<X className="h-3.5 w-3.5" aria-hidden="true" />
<span>{disconnectLabel}</span>
</button>
)}
<button
type="button"
onClick={onRestart}
disabled={disableRestart}
className="flex items-center space-x-1 text-xs text-gray-400 hover:text-white disabled:cursor-not-allowed disabled:opacity-50"
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"
title={restartTitle}
>
<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>
<RotateCcw className={`h-3.5 w-3.5 ${isRestarting ? 'animate-spin' : ''}`} aria-hidden="true" />
<span>{restartLabel}</span>
</button>
</div>

View File

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

View File

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

View File

@@ -138,42 +138,6 @@
"clearInput": "Eingabe leeren",
"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": {
"title": "KI-Assistent wählen",
"description": "Anbieter auswählen, um eine neue Unterhaltung zu starten",

View File

@@ -139,42 +139,6 @@
"clearInput": "Clear input",
"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": {
"title": "Choose Your AI Assistant",
"description": "Select a provider to start a new conversation",
@@ -229,7 +193,7 @@
"disconnect": "Disconnect",
"disconnectTitle": "Disconnect from shell",
"restart": "Restart",
"restartTitle": "Restart Shell (disconnect first)",
"restartTitle": "Restart Shell",
"connect": "Continue in Shell",
"connectTitle": "Connect to shell"
},

View File

@@ -138,42 +138,6 @@
"clearInput": "Cancella input",
"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": {
"title": "Scegli il tuo assistente AI",
"description": "Seleziona un provider per iniziare una nuova conversazione",

View File

@@ -117,42 +117,6 @@
"clickToChangeMode": "クリックで権限モードを変更または入力欄でTab",
"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": {
"title": "AIアシスタントを選択",
"description": "新しい会話を始めるプロバイダーを選択してください",

View File

@@ -120,42 +120,6 @@
"clearInput": "입력 지우기",
"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": {
"title": "AI 어시스턴트 선택",
"description": "새 대화를 시작할 프로바이더를 선택하세요",

View File

@@ -138,42 +138,6 @@
"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": "Выберите провайдера для начала нового разговора",

View File

@@ -138,42 +138,6 @@
"clearInput": "Girdiyi temizle",
"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": {
"title": "AI Asistanını Seç",
"description": "Yeni bir konuşma başlatmak için bir sağlayıcı seç",

View File

@@ -120,42 +120,6 @@
"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": "选择一个供应商以开始新对话",

View File

@@ -0,0 +1,37 @@
{
"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

@@ -0,0 +1,257 @@
{
"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

@@ -0,0 +1,34 @@
{
"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

@@ -0,0 +1,267 @@
{
"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

@@ -0,0 +1,484 @@
{
"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

@@ -0,0 +1,136 @@
{
"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

@@ -0,0 +1,142 @@
{
"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": "嘗試調整您的搜尋或篩選條件。"
}
}