Merge branch 'main' into feat/notifications

This commit is contained in:
viper151
2026-03-02 16:04:55 +01:00
committed by GitHub
12 changed files with 105 additions and 20 deletions

View File

@@ -3,6 +3,23 @@
All notable changes to CloudCLI UI will be documented in this file. All notable changes to CloudCLI UI will be documented in this file.
## [1.21.0](https://github.com/siteboon/claudecodeui/compare/v1.20.1...v1.21.0) (2026-02-27)
### New Features
* add copy icon for user messages ([#449](https://github.com/siteboon/claudecodeui/issues/449)) ([b359c51](https://github.com/siteboon/claudecodeui/commit/b359c515277b4266fde2fb9a29b5356949c07c4f))
* Google's gemini-cli integration ([#422](https://github.com/siteboon/claudecodeui/issues/422)) ([a367edd](https://github.com/siteboon/claudecodeui/commit/a367edd51578608b3281373cb4a95169dbf17f89))
* persist active tab across reloads via localStorage ([#414](https://github.com/siteboon/claudecodeui/issues/414)) ([e3b6892](https://github.com/siteboon/claudecodeui/commit/e3b689214f11d549ffe1b3a347476d58f25c5aca)), closes [#387](https://github.com/siteboon/claudecodeui/issues/387)
### Bug Fixes
* add support for Codex in the shell ([#424](https://github.com/siteboon/claudecodeui/issues/424)) ([23801e9](https://github.com/siteboon/claudecodeui/commit/23801e9cc15d2b8d1bfc6e39aee2fae93226d1ad))
### Maintenance
* upgrade @anthropic-ai/claude-agent-sdk to version 0.2.59 and add model usage logging ([#446](https://github.com/siteboon/claudecodeui/issues/446)) ([917c353](https://github.com/siteboon/claudecodeui/commit/917c353115653ee288bf97be01f62fad24123cbc))
* upgrade better-sqlite to latest version to support node 25 ([#445](https://github.com/siteboon/claudecodeui/issues/445)) ([4ab94fc](https://github.com/siteboon/claudecodeui/commit/4ab94fce4257e1e20370fa83fa4c0f6fadbb8a2b))
## [1.20.1](https://github.com/siteboon/claudecodeui/compare/v1.19.1...v1.20.1) (2026-02-23) ## [1.20.1](https://github.com/siteboon/claudecodeui/compare/v1.19.1...v1.20.1) (2026-02-23)
### New Features ### New Features

View File

@@ -4,7 +4,7 @@
</div> </div>
A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Cursor CLI](https://docs.cursor.com/en/cli/overview) and [Codex](https://developers.openai.com/codex). You can use it locally or remotely to view your active projects and sessions in Claude Code, Cursor, or Codex and make changes to them from everywhere (mobile or desktop). This gives you a proper interface that works everywhere. A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Cursor CLI](https://docs.cursor.com/en/cli/overview), [Codex](https://developers.openai.com/codex), and [Gemini-CLI](https://geminicli.com/). You can use it locally or remotely to view your active projects and sessions and make changes to them from everywhere (mobile or desktop). This gives you a proper interface that works everywhere.
<a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/15586" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15586" alt="siteboon%2Fclaudecodeui | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<div align="right"><i><b>English</b> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">中文</a> · <a href="./README.ja.md">日本語</a></i></div> <div align="right"><i><b>English</b> · <a href="./README.ko.md">한국어</a> · <a href="./README.zh-CN.md">中文</a> · <a href="./README.ja.md">日本語</a></i></div>
@@ -44,14 +44,14 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla
## Features ## Features
- **Responsive Design** - Works seamlessly across desktop, tablet, and mobile so you can also use Claude Code, Cursor, or Codex from mobile - **Responsive Design** - Works seamlessly across desktop, tablet, and mobile so you can also use Agents from mobile
- **Interactive Chat Interface** - Built-in chat interface for seamless communication with Claude Code, Cursor, or Codex - **Interactive Chat Interface** - Built-in chat interface for seamless communication with the Agents
- **Integrated Shell Terminal** - Direct access to Claude Code, Cursor CLI, or Codex through built-in shell functionality - **Integrated Shell Terminal** - Direct access to the Agents CLI through built-in shell functionality
- **File Explorer** - Interactive file tree with syntax highlighting and live editing - **File Explorer** - Interactive file tree with syntax highlighting and live editing
- **Git Explorer** - View, stage and commit your changes. You can also switch branches - **Git Explorer** - View, stage and commit your changes. You can also switch branches
- **Session Management** - Resume conversations, manage multiple sessions, and track history - **Session Management** - Resume conversations, manage multiple sessions, and track history
- **TaskMaster AI Integration** *(Optional)* - Advanced project management with AI-powered task planning, PRD parsing, and workflow automation - **TaskMaster AI Integration** *(Optional)* - Advanced project management with AI-powered task planning, PRD parsing, and workflow automation
- **Model Compatibility** - Works with Claude Sonnet 4.5, Opus 4.5, and GPT-5.2 - **Model Compatibility** - Works with Claude Sonnet 4.5, Opus 4.5, GPT-5.2, and Gemini.
## Quick Start ## Quick Start
@@ -61,7 +61,8 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla
- [Node.js](https://nodejs.org/) v22 or higher - [Node.js](https://nodejs.org/) v22 or higher
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured, and/or - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured, and/or
- [Cursor CLI](https://docs.cursor.com/en/cli/overview) installed and configured, and/or - [Cursor CLI](https://docs.cursor.com/en/cli/overview) installed and configured, and/or
- [Codex](https://developers.openai.com/codex) installed and configured - [Codex](https://developers.openai.com/codex) installed and configured, and/or
- [Gemini-CLI](https://geminicli.com/) installed and configured
### One-click Operation (Recommended) ### One-click Operation (Recommended)
@@ -279,7 +280,7 @@ session counts
### Backend (Node.js + Express) ### Backend (Node.js + Express)
- **Express Server** - RESTful API with static file serving - **Express Server** - RESTful API with static file serving
- **WebSocket Server** - Communication for chats and project refresh - **WebSocket Server** - Communication for chats and project refresh
- **Agent Integration (Claude Code / Cursor CLI / Codex)** - Process spawning and management - **Agent Integration (Claude Code / Cursor CLI / Codex / Gemini CLI)** - Process spawning and management
- **File System API** - Exposing file browser for projects - **File System API** - Exposing file browser for projects
### Frontend (React + Vite) ### Frontend (React + Vite)
@@ -327,6 +328,7 @@ This project is open source and free to use, modify, and distribute under the GP
- **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** - Anthropic's official CLI - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** - Anthropic's official CLI
- **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** - Cursor's official CLI - **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** - Cursor's official CLI
- **[Codex](https://developers.openai.com/codex)** - OpenAI Codex - **[Codex](https://developers.openai.com/codex)** - OpenAI Codex
- **[Gemini-CLI](https://geminicli.com/)** - Google Gemini CLI
- **[React](https://react.dev/)** - User interface library - **[React](https://react.dev/)** - User interface library
- **[Vite](https://vitejs.dev/)** - Fast build tool and dev server - **[Vite](https://vitejs.dev/)** - Fast build tool and dev server
- **[Tailwind CSS](https://tailwindcss.com/)** - Utility-first CSS framework - **[Tailwind CSS](https://tailwindcss.com/)** - Utility-first CSS framework

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@siteboon/claude-code-ui", "name": "@siteboon/claude-code-ui",
"version": "1.20.1", "version": "1.21.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@siteboon/claude-code-ui", "name": "@siteboon/claude-code-ui",
"version": "1.20.1", "version": "1.21.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@siteboon/claude-code-ui", "name": "@siteboon/claude-code-ui",
"version": "1.20.1", "version": "1.21.0",
"description": "A web-based UI for Claude Code CLI", "description": "A web-based UI for Claude Code CLI",
"type": "module", "type": "module",
"main": "server/index.js", "main": "server/index.js",
@@ -119,4 +119,4 @@
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.0.4" "vite": "^7.0.4"
} }
} }

View File

@@ -660,9 +660,6 @@ async function queryClaudeSDK(command, options = {}, ws) {
console.log('No session_id in message or already captured. message.session_id:', message.session_id, 'capturedSessionId:', capturedSessionId); console.log('No session_id in message or already captured. message.session_id:', message.session_id, 'capturedSessionId:', capturedSessionId);
} }
// logs which model was used in the message
console.log("---> Model was sent using:", Object.keys(message.modelUsage || {}));
// Transform and send message to WebSocket // Transform and send message to WebSocket
const transformedMessage = transformMessage(message); const transformedMessage = transformMessage(message);
ws.send({ ws.send({
@@ -673,6 +670,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
// Extract and send token budget updates from result messages // Extract and send token budget updates from result messages
if (message.type === 'result') { if (message.type === 'result') {
const models = Object.keys(message.modelUsage || {});
if (models.length > 0) {
console.log("---> Model was sent using:", models);
}
const tokenBudget = extractTokenBudget(message); const tokenBudget = extractTokenBudget(message);
if (tokenBudget) { if (tokenBudget) {
console.log('Token budget from modelUsage:', tokenBudget); console.log('Token budget from modelUsage:', tokenBudget);

View File

@@ -1220,7 +1220,7 @@ function handleShellConnection(ws) {
if (hasSession && sessionId) { if (hasSession && sessionId) {
try { try {
// Gemini CLI enforces its own native session IDs, unlike other agents that accept arbitrary string names. // Gemini CLI enforces its own native session IDs, unlike other agents that accept arbitrary string names.
// The UI only knows about its internal generated `sessionId` (e.g. gemini_1234). // The UI only knows about its internal generated `sessionId` (e.g. gemini_1234).
// We must fetch the mapping from the backend session manager to pass the native `cliSessionId` to the shell. // We must fetch the mapping from the backend session manager to pass the native `cliSessionId` to the shell.
const sess = sessionManager.getSession(sessionId); const sess = sessionManager.getSession(sessionId);
if (sess && sess.cliSessionId) { if (sess && sess.cliSessionId) {
@@ -1793,8 +1793,8 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
// Construct the JSONL file path // Construct the JSONL file path
// Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl // Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
// The encoding replaces /, spaces, ~, and _ with - // The encoding replaces any non-alphanumeric character (except -) with -
const encodedPath = projectPath.replace(/[\\/:\s~_]/g, '-'); const encodedPath = projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath); const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`); const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);

View File

@@ -10,6 +10,7 @@ import type {
import { Markdown } from './Markdown'; import { Markdown } from './Markdown';
import { formatUsageLimitText } from '../../utils/chatFormatting'; import { formatUsageLimitText } from '../../utils/chatFormatting';
import { getClaudePermissionSuggestion } from '../../utils/chatPermissions'; import { getClaudePermissionSuggestion } from '../../utils/chatPermissions';
import { copyTextToClipboard } from '../../../../utils/clipboard';
import type { Project } from '../../../../types/app'; import type { Project } from '../../../../types/app';
import { ToolRenderer, shouldHideToolResult } from '../../tools'; import { ToolRenderer, shouldHideToolResult } from '../../tools';
@@ -53,6 +54,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
const [isExpanded, setIsExpanded] = React.useState(false); const [isExpanded, setIsExpanded] = React.useState(false);
const permissionSuggestion = getClaudePermissionSuggestion(message, provider); const permissionSuggestion = getClaudePermissionSuggestion(message, provider);
const [permissionGrantState, setPermissionGrantState] = React.useState<PermissionGrantState>('idle'); const [permissionGrantState, setPermissionGrantState] = React.useState<PermissionGrantState>('idle');
const [messageCopied, setMessageCopied] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
@@ -100,7 +102,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
{message.type === 'user' ? ( {message.type === 'user' ? (
/* User message bubble on the right */ /* User message bubble on the right */
<div className="flex items-end space-x-0 sm:space-x-3 w-full sm:w-auto sm:max-w-[85%] md:max-w-md lg:max-w-lg xl:max-w-xl"> <div className="flex items-end space-x-0 sm:space-x-3 w-full sm:w-auto sm:max-w-[85%] md:max-w-md lg:max-w-lg xl:max-w-xl">
<div className="bg-blue-600 text-white rounded-2xl rounded-br-md px-3 sm:px-4 py-2 shadow-sm flex-1 sm:flex-initial"> <div className="bg-blue-600 text-white rounded-2xl rounded-br-md px-3 sm:px-4 py-2 shadow-sm flex-1 sm:flex-initial group">
<div className="text-sm whitespace-pre-wrap break-words"> <div className="text-sm whitespace-pre-wrap break-words">
{message.content} {message.content}
</div> </div>
@@ -117,8 +119,45 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
))} ))}
</div> </div>
)} )}
<div className="text-xs text-blue-100 mt-1 text-right"> <div className="flex items-center justify-end gap-1 mt-1 text-xs text-blue-100">
{formattedTime} <button
type="button"
onClick={() => {
const text = String(message.content || '');
if (!text) return;
copyTextToClipboard(text).then((success) => {
if (!success) return;
setMessageCopied(true);
});
}}
title={messageCopied ? t('copyMessage.copied') : t('copyMessage.copy')}
aria-label={messageCopied ? t('copyMessage.copied') : t('copyMessage.copy')}
>
{messageCopied ? (
<svg className="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
) : (
<svg
className="w-3.5 h-3.5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
</svg>
)}
</button>
<span>{formattedTime}</span>
</div> </div>
</div> </div>
{!isGrouped && ( {!isGrouped && (

View File

@@ -1,3 +1,4 @@
import { useEffect } from 'react';
import type { TFunction } from 'i18next'; import type { TFunction } from 'i18next';
import type { LoadingProgress, Project, ProjectSession, SessionProvider } from '../../../../types/app'; import type { LoadingProgress, Project, ProjectSession, SessionProvider } from '../../../../types/app';
import type { import type {
@@ -103,6 +104,15 @@ export default function SidebarProjectList({
/> />
); );
useEffect(() => {
let baseTitle = 'CloudCLI UI';
const displayName = selectedProject?.displayName?.trim();
if (displayName) {
baseTitle = `${displayName} - ${baseTitle}`;
}
document.title = baseTitle;
}, [selectedProject]);
const showProjects = !isLoading && projects.length > 0 && filteredProjects.length > 0; const showProjects = !isLoading && projects.length > 0 && filteredProjects.length > 0;
return ( return (

View File

@@ -4,6 +4,10 @@
"copied": "Copied", "copied": "Copied",
"copyCode": "Copy code" "copyCode": "Copy code"
}, },
"copyMessage": {
"copy": "Copy message",
"copied": "Message copied"
},
"messageTypes": { "messageTypes": {
"user": "U", "user": "U",
"error": "Error", "error": "Error",

View File

@@ -4,6 +4,10 @@
"copied": "コピーしました", "copied": "コピーしました",
"copyCode": "コードをコピー" "copyCode": "コードをコピー"
}, },
"copyMessage": {
"copy": "メッセージをコピー",
"copied": "メッセージをコピーしました"
},
"messageTypes": { "messageTypes": {
"user": "U", "user": "U",
"error": "エラー", "error": "エラー",

View File

@@ -4,6 +4,10 @@
"copied": "복사됨", "copied": "복사됨",
"copyCode": "코드 복사" "copyCode": "코드 복사"
}, },
"copyMessage": {
"copy": "메시지 복사",
"copied": "메시지 복사됨"
},
"messageTypes": { "messageTypes": {
"user": "U", "user": "U",
"error": "오류", "error": "오류",

View File

@@ -4,6 +4,10 @@
"copied": "已复制", "copied": "已复制",
"copyCode": "复制代码" "copyCode": "复制代码"
}, },
"copyMessage": {
"copy": "复制消息",
"copied": "消息已复制"
},
"messageTypes": { "messageTypes": {
"user": "U", "user": "U",
"error": "错误", "error": "错误",