Compare commits

..

1 Commits

Author SHA1 Message Date
Haileyesus
3576662e25 fix: remove thinking mode 2026-06-05 15:28:58 +03:00
23 changed files with 731 additions and 989 deletions

771
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -67,7 +67,7 @@
"author": "CloudCLI UI Contributors",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.3.165",
"@anthropic-ai/claude-agent-sdk": "^0.2.116",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",

View File

@@ -1483,133 +1483,74 @@ 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
let entries;
const items = [];
try {
await acquire();
try {
entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
} finally {
release();
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);
}
} 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,7 +18,6 @@ type ShellIncomingMessage = {
provider?: string;
initialCommand?: string;
isPlainShell?: boolean;
forceRestart?: boolean;
};
type PtySessionEntry = {
@@ -181,7 +180,6 @@ 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) ||
@@ -202,7 +200,7 @@ export function handleShellConnection(
: '';
ptySessionKey = `${projectPath}_${sessionId ?? 'default'}${commandSuffix}`;
if (isLoginCommand || forceRestart) {
if (isLoginCommand) {
const oldSession = ptySessionsMap.get(ptySessionKey);
if (oldSession) {
if (oldSession.timeoutId) {
@@ -213,8 +211,7 @@ export function handleShellConnection(
}
}
const existingSession =
isLoginCommand || forceRestart ? null : ptySessionsMap.get(ptySessionKey);
const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
if (existingSession) {
shellProcess = existingSession.pty;
if (existingSession.timeoutId) {
@@ -371,10 +368,6 @@ 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({
@@ -458,10 +451,6 @@ 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

@@ -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);
@@ -562,11 +560,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 +743,6 @@ export function useChatComposerState({
setUploadingImages(new Map());
setImageErrors(new Map());
setIsTextareaExpanded(false);
setThinkingMode('none');
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
@@ -783,7 +776,6 @@ export function useChatComposerState({
setIsLoading,
setIsUserScrolledUp,
slashCommands,
thinkingMode,
],
);
@@ -1020,8 +1012,6 @@ export function useChatComposerState({
textareaRef,
inputHighlightRef,
isTextareaExpanded,
thinkingMode,
setThinkingMode,
slashCommandsCount,
filteredCommands,
frequentCommands,

View File

@@ -141,8 +141,6 @@ function ChatInterface({
textareaRef,
inputHighlightRef,
isTextareaExpanded,
thinkingMode,
setThinkingMode,
slashCommandsCount,
filteredCommands,
frequentCommands,
@@ -369,8 +367,6 @@ function ChatInterface({
provider={provider}
permissionMode={permissionMode}
onModeSwitch={cyclePermissionMode}
thinkingMode={thinkingMode}
setThinkingMode={setThinkingMode}
tokenBudget={tokenBudget}
slashCommandsCount={slashCommandsCount}
onToggleCommandMenu={handleToggleCommandMenu}

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,8 +57,6 @@ interface ChatComposerProps {
provider: Provider | string;
permissionMode: PermissionMode | string;
onModeSwitch: () => void;
thinkingMode: string;
setThinkingMode: Dispatch<SetStateAction<string>>;
tokenBudget: Record<string, unknown> | null;
slashCommandsCount: number;
onToggleCommandMenu: () => void;
@@ -113,8 +110,6 @@ export default function ChatComposer({
provider,
permissionMode,
onModeSwitch,
thinkingMode,
setThinkingMode,
tokenBudget,
slashCommandsCount,
onToggleCommandMenu,
@@ -358,10 +353,6 @@ export default function ChatComposer({
</div>
</button>
{provider === 'claude' && (
<ThinkingModeSelector selectedMode={thinkingMode} onModeChange={setThinkingMode} onClose={() => {}} className="" />
)}
<TokenUsageSummary usage={tokenBudget} />
<PromptInputButton
@@ -383,7 +374,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 +391,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

@@ -321,7 +321,6 @@ 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,7 +2,6 @@ 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';
@@ -32,8 +31,8 @@ type UseShellConnectionResult = {
isConnected: boolean;
isConnecting: boolean;
closeSocket: () => void;
connectToShell: (options?: { forceRestart?: boolean }) => void;
disconnectFromShell: (options?: { suppressAutoConnect?: boolean }) => void;
connectToShell: () => void;
disconnectFromShell: () => void;
};
export function useShellConnection({
@@ -55,8 +54,6 @@ 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) => {
@@ -144,8 +141,6 @@ export function useShellConnection({
}
currentFitAddon.fit();
const forceRestart = forceRestartOnInitRef.current;
forceRestartOnInitRef.current = false;
sendSocketMessage(socket, {
type: 'init',
@@ -157,7 +152,6 @@ export function useShellConnection({
rows: currentTerminal.rows,
initialCommand: initialCommandRef.current,
isPlainShell: isPlainShellRef.current,
forceRestart,
});
}, TERMINAL_INIT_DELAY_MS);
};
@@ -183,7 +177,6 @@ export function useShellConnection({
setIsConnected(false);
setIsConnecting(false);
connectingRef.current = false;
forceRestartOnInitRef.current = false;
}
},
[
@@ -202,40 +195,27 @@ export function useShellConnection({
],
);
const connectToShell = useCallback((options?: { forceRestart?: boolean }) => {
const connectToShell = useCallback(() => {
if (!isInitialized || isConnected || isConnecting || connectingRef.current) {
return;
}
forceRestartOnInitRef.current = Boolean(options?.forceRestart);
suppressAutoConnectRef.current = false;
connectingRef.current = true;
setIsConnecting(true);
connectWebSocket(true);
}, [connectWebSocket, isConnected, isConnecting, isInitialized]);
const disconnectFromShell = useCallback((options?: { suppressAutoConnect?: boolean }) => {
if (options?.suppressAutoConnect) {
suppressAutoConnectRef.current = true;
}
const disconnectFromShell = useCallback(() => {
closeSocket();
clearTerminalScreen();
setIsConnected(false);
setIsConnecting(false);
connectingRef.current = false;
forceRestartOnInitRef.current = false;
setAuthUrl('');
}, [clearTerminalScreen, closeSocket, setAuthUrl]);
useEffect(() => {
if (
!autoConnect ||
suppressAutoConnectRef.current ||
!isInitialized ||
isConnecting ||
isConnected
) {
if (!autoConnect || !isInitialized || isConnecting || isConnected) {
return;
}

View File

@@ -1,7 +1,6 @@
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';
@@ -16,7 +15,6 @@ export type ShellInitMessage = {
rows: number;
initialCommand: string | null | undefined;
isPlainShell: boolean;
forceRestart?: boolean;
};
export type ShellResizeMessage = {
@@ -71,8 +69,8 @@ export type UseShellRuntimeResult = {
isConnecting: boolean;
authUrl: string;
authUrlVersion: number;
connectToShell: (options?: { forceRestart?: boolean }) => void;
disconnectFromShell: (options?: { suppressAutoConnect?: boolean }) => void;
connectToShell: () => void;
disconnectFromShell: () => void;
openAuthUrlInBrowser: (url?: string) => boolean;
copyAuthUrlToClipboard: (url?: string) => Promise<boolean>;
};

View File

@@ -1,6 +1,5 @@
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 {
@@ -14,7 +13,6 @@ 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';
@@ -48,8 +46,6 @@ 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 {
@@ -144,7 +140,6 @@ export default function Shell({
useEffect(() => {
return () => {
if (promptCheckTimer.current) clearTimeout(promptCheckTimer.current);
if (restartTimerRef.current) clearTimeout(restartTimerRef.current);
};
}, []);
@@ -195,42 +190,12 @@ export default function Shell({
);
const handleRestartShell = useCallback(() => {
restartAfterInitRef.current = true;
setIsRestarting(true);
if (restartTimerRef.current) {
clearTimeout(restartTimerRef.current);
}
restartTimerRef.current = setTimeout(() => {
window.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
@@ -289,7 +254,7 @@ export default function Shell({
isRestarting={isRestarting}
hasSession={Boolean(selectedSession)}
sessionDisplayNameShort={sessionDisplayNameShort}
onDisconnect={handleDisconnectShell}
onDisconnect={disconnectFromShell}
onRestart={handleRestartShell}
statusNewSessionText={t('shell.status.newSession')}
statusInitializingText={t('shell.status.initializing')}
@@ -298,7 +263,7 @@ export default function Shell({
disconnectTitle={t('shell.actions.disconnectTitle')}
restartLabel={t('shell.actions.restart')}
restartTitle={t('shell.actions.restartTitle')}
disableRestart={isRestarting || !isInitialized}
disableRestart={isRestarting || isConnected}
/>
<div className="relative flex-1 overflow-hidden p-2">
@@ -316,7 +281,7 @@ export default function Shell({
connectLabel={t('shell.actions.connect')}
connectTitle={t('shell.actions.connectTitle')}
connectingLabel={t('shell.connecting')}
onConnect={handleRestartShell}
onConnect={connectToShell}
/>
)}

View File

@@ -1,5 +1,3 @@
import { Loader2, RotateCcw } from 'lucide-react';
type ShellConnectionOverlayProps = {
mode: 'loading' | 'connect' | 'connecting';
description: string;
@@ -21,42 +19,40 @@ export default function ShellConnectionOverlay({
}: ShellConnectionOverlayProps) {
if (mode === 'loading') {
return (
<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 className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90">
<div className="text-white">{loadingLabel}</div>
</div>
);
}
if (mode === 'connect') {
return (
<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="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">
<button
type="button"
onClick={onConnect}
className="pointer-events-auto inline-flex min-h-12 w-full max-w-xs cursor-pointer items-center justify-center gap-2 rounded-md bg-emerald-600 px-5 py-3 text-base font-semibold text-white shadow-lg shadow-emerald-950/30 transition-colors hover:bg-emerald-500 focus:outline-none focus:ring-2 focus:ring-emerald-300 focus:ring-offset-2 focus:ring-offset-gray-950 active:bg-emerald-700"
className="flex w-full items-center justify-center space-x-2 rounded-lg bg-green-600 px-6 py-3 text-base font-medium text-white transition-colors hover:bg-green-700 sm:w-auto"
title={connectTitle}
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
<span className="min-w-0 truncate">{connectLabel}</span>
<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>
</button>
<p className="max-w-md break-words px-2 text-sm leading-6 text-gray-300">{description}</p>
<p className="mt-3 px-2 text-sm text-gray-400">{description}</p>
</div>
</div>
);
}
return (
<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" />
<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>
<span className="text-base font-medium">{connectingLabel}</span>
</div>
<p className="max-w-md break-words px-2 text-sm leading-6 text-gray-300">{description}</p>
<p className="mt-3 px-2 text-sm text-gray-400">{description}</p>
</div>
</div>
);

View File

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

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",
"restartTitle": "Restart Shell (disconnect first)",
"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": "选择一个供应商以开始新对话",