mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-11 16:11:31 +00:00
Compare commits
4 Commits
refactor/r
...
fix/thinki
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba7c628d35 | ||
|
|
e2459cb0f8 | ||
|
|
9552577e94 | ||
|
|
590dd42649 |
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -20,14 +20,14 @@ jobs:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: git config
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -3,6 +3,21 @@
|
||||
All notable changes to CloudCLI UI will be documented in this file.
|
||||
|
||||
|
||||
## [1.28.1](https://github.com/siteboon/claudecodeui/compare/v1.28.0...v1.28.1) (2026-04-10)
|
||||
|
||||
### New Features
|
||||
|
||||
* add branding, community links, GitHub star badge, and About settings tab ([2207d05](https://github.com/siteboon/claudecodeui/commit/2207d05c1ca229214aa9c2e2c9f4d0827d421574))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* corrupted binary downloads ([#634](https://github.com/siteboon/claudecodeui/issues/634)) ([e61f8a5](https://github.com/siteboon/claudecodeui/commit/e61f8a543d63fe7c24a04b3d2186085a06dcbcdb))
|
||||
* **ui:** remove mobile bottom nav, unify processing indicator, and improve tooltip behavior on mobile ([#632](https://github.com/siteboon/claudecodeui/issues/632)) ([a8dab0e](https://github.com/siteboon/claudecodeui/commit/a8dab0edcf949ae610820bae9500c433781f7c73))
|
||||
|
||||
### Refactoring
|
||||
|
||||
* remove unused whispher transcribe logic ([#637](https://github.com/siteboon/claudecodeui/issues/637)) ([590dd42](https://github.com/siteboon/claudecodeui/commit/590dd42649424ab990353fcf59ce0965036d3d25))
|
||||
|
||||
## [1.28.0](https://github.com/siteboon/claudecodeui/compare/v1.27.1...v1.28.0) (2026-04-03)
|
||||
|
||||
### New Features
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@cloudcli-ai/cloudcli",
|
||||
"version": "1.28.0",
|
||||
"version": "1.28.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@cloudcli-ai/cloudcli",
|
||||
"version": "1.28.0",
|
||||
"version": "1.28.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cloudcli-ai/cloudcli",
|
||||
"version": "1.28.0",
|
||||
"version": "1.28.1",
|
||||
"description": "A web-based UI for Claude Code CLI",
|
||||
"type": "module",
|
||||
"main": "server/index.js",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
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';
|
||||
@@ -12,6 +13,11 @@ type ThinkingModeSelectorProps = {
|
||||
|
||||
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> = {
|
||||
@@ -29,50 +35,143 @@ function ThinkingModeSelector({ selectedMode, onModeChange, onClose, className =
|
||||
};
|
||||
});
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
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(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
if (onClose) onClose();
|
||||
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('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [onClose]);
|
||||
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={dropdownRef}>
|
||||
<div className={`relative ${className}`} ref={containerRef}>
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
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 && (
|
||||
<div className="absolute bottom-full right-0 mb-2 w-64 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800">
|
||||
{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
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
if (onClose) onClose();
|
||||
}}
|
||||
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" />
|
||||
@@ -83,7 +182,7 @@ function ThinkingModeSelector({ selectedMode, onModeChange, onClose, className =
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="py-1">
|
||||
<div className="min-h-0 overflow-y-auto py-1">
|
||||
{translatedModes.map((mode) => {
|
||||
const ModeIcon = mode.icon;
|
||||
const isSelected = mode.id === selectedMode;
|
||||
@@ -91,10 +190,10 @@ function ThinkingModeSelector({ selectedMode, onModeChange, onClose, className =
|
||||
return (
|
||||
<button
|
||||
key={mode.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onModeChange(mode.id);
|
||||
setIsOpen(false);
|
||||
if (onClose) onClose();
|
||||
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' : ''
|
||||
}`}
|
||||
@@ -135,10 +234,11 @@ function ThinkingModeSelector({ selectedMode, onModeChange, onClose, className =
|
||||
<strong>Tip:</strong> {t('thinkingMode.selector.tip')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ThinkingModeSelector;
|
||||
export default ThinkingModeSelector;
|
||||
|
||||
Reference in New Issue
Block a user