mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-06 16:31:12 +08:00
feat(cli-tools): add effort level configuration for Claude CLI
- Introduced effort level options (low, medium, high) in the CLI tool settings. - Updated the SettingsPage and CliToolCard components to handle effort level updates. - Enhanced CLI command options to accept effort level via --effort parameter. - Modified backend routes to support effort level updates in tool configurations. - Created a new CliViewerToolbar component for improved CLI viewer interactions. - Implemented logic to manage and display execution statuses and layouts in the CLI viewer.
This commit is contained in:
@@ -5,29 +5,9 @@
|
||||
// Integrates with viewerStore for state management
|
||||
// Includes WebSocket integration and execution recovery
|
||||
|
||||
import { useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
Terminal,
|
||||
LayoutGrid,
|
||||
Columns,
|
||||
Rows,
|
||||
Square,
|
||||
ChevronDown,
|
||||
RotateCcw,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
} from '@/components/ui/Dropdown';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { LayoutContainer } from '@/components/cli-viewer';
|
||||
import { LayoutContainer, CliViewerToolbar } from '@/components/cli-viewer';
|
||||
import {
|
||||
useViewerStore,
|
||||
useViewerLayout,
|
||||
@@ -43,14 +23,6 @@ import { useActiveCliExecutions, useInvalidateActiveCliExecutions } from '@/hook
|
||||
// Types
|
||||
// ========================================
|
||||
|
||||
export type LayoutType = 'single' | 'split-h' | 'split-v' | 'grid-2x2';
|
||||
|
||||
interface LayoutOption {
|
||||
id: LayoutType;
|
||||
icon: React.ElementType;
|
||||
labelKey: string;
|
||||
}
|
||||
|
||||
// CLI WebSocket message types (matching CliStreamMonitorLegacy)
|
||||
interface CliStreamStartedPayload {
|
||||
executionId: string;
|
||||
@@ -86,14 +58,7 @@ interface CliStreamErrorPayload {
|
||||
// Constants
|
||||
// ========================================
|
||||
|
||||
const LAYOUT_OPTIONS: LayoutOption[] = [
|
||||
{ id: 'single', icon: Square, labelKey: 'cliViewer.layout.single' },
|
||||
{ id: 'split-h', icon: Columns, labelKey: 'cliViewer.layout.splitH' },
|
||||
{ id: 'split-v', icon: Rows, labelKey: 'cliViewer.layout.splitV' },
|
||||
{ id: 'grid-2x2', icon: LayoutGrid, labelKey: 'cliViewer.layout.grid' },
|
||||
];
|
||||
|
||||
const DEFAULT_LAYOUT: LayoutType = 'split-h';
|
||||
const DEFAULT_LAYOUT = 'split-h' as const;
|
||||
|
||||
// ========================================
|
||||
// Helper Functions
|
||||
@@ -111,41 +76,6 @@ function formatDuration(ms: number): string {
|
||||
return `${hours}h ${remainingMinutes}m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect layout type from AllotmentLayout structure
|
||||
*/
|
||||
function detectLayoutType(layout: AllotmentLayout): LayoutType {
|
||||
const childCount = layout.children.length;
|
||||
|
||||
// Empty or single pane
|
||||
if (childCount === 0 || childCount === 1) {
|
||||
return 'single';
|
||||
}
|
||||
|
||||
// Two panes at root level
|
||||
if (childCount === 2) {
|
||||
const hasNestedGroups = layout.children.some(
|
||||
(child) => typeof child !== 'string'
|
||||
);
|
||||
|
||||
// If no nested groups, it's a simple split
|
||||
if (!hasNestedGroups) {
|
||||
return layout.direction === 'horizontal' ? 'split-h' : 'split-v';
|
||||
}
|
||||
|
||||
// Check for grid layout (2x2)
|
||||
const allNested = layout.children.every(
|
||||
(child) => typeof child !== 'string'
|
||||
);
|
||||
if (allNested) {
|
||||
return 'grid-2x2';
|
||||
}
|
||||
}
|
||||
|
||||
// Default to current direction
|
||||
return layout.direction === 'horizontal' ? 'split-h' : 'split-v';
|
||||
}
|
||||
|
||||
/**
|
||||
* Count total panes in layout
|
||||
*/
|
||||
@@ -169,14 +99,16 @@ function countPanes(layout: AllotmentLayout): number {
|
||||
// ========================================
|
||||
|
||||
export function CliViewerPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
// Fullscreen state
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
|
||||
// Store hooks
|
||||
const layout = useViewerLayout();
|
||||
const panes = useViewerPanes();
|
||||
const focusedPaneId = useFocusedPaneId();
|
||||
const { initializeDefaultLayout, addTab, reset } = useViewerStore();
|
||||
const { initializeDefaultLayout, addTab } = useViewerStore();
|
||||
|
||||
// CLI Stream Store hooks
|
||||
const executions = useCliStreamStore((state) => state.executions);
|
||||
@@ -188,24 +120,9 @@ export function CliViewerPage() {
|
||||
const lastMessage = useNotificationStore(selectWsLastMessage);
|
||||
|
||||
// Active execution sync from server
|
||||
const { isLoading: _isSyncing } = useActiveCliExecutions(true); // Always sync when page is open
|
||||
useActiveCliExecutions(true);
|
||||
const invalidateActive = useInvalidateActiveCliExecutions();
|
||||
|
||||
// Detect current layout type from store
|
||||
const currentLayoutType = useMemo(() => detectLayoutType(layout), [layout]);
|
||||
|
||||
// Count active sessions (tabs across all panes)
|
||||
const activeSessionCount = useMemo(() => {
|
||||
return Object.values(panes).reduce((count, pane) => count + pane.tabs.length, 0);
|
||||
}, [panes]);
|
||||
|
||||
// Get execution count for display
|
||||
const executionCount = useMemo(() => Object.keys(executions).length, [executions]);
|
||||
const runningCount = useMemo(
|
||||
() => Object.values(executions).filter(e => e.status === 'running').length,
|
||||
[executions]
|
||||
);
|
||||
|
||||
// Handle WebSocket messages for CLI stream (same logic as CliStreamMonitorLegacy)
|
||||
useEffect(() => {
|
||||
if (!lastMessage || lastMessage === lastProcessedMsgRef.current) return;
|
||||
@@ -297,30 +214,22 @@ export function CliViewerPage() {
|
||||
}, [lastMessage, invalidateActive]);
|
||||
|
||||
// Auto-add new executions as tabs, distributing across available panes
|
||||
// Uses round-robin distribution to spread executions across panes side-by-side
|
||||
const addedExecutionsRef = useRef<Set<string>>(new Set());
|
||||
useEffect(() => {
|
||||
// Get all pane IDs from the current layout
|
||||
const paneIds = Object.keys(panes);
|
||||
if (paneIds.length === 0) return;
|
||||
|
||||
// Get addTab from store directly to avoid dependency on reactive function
|
||||
// This prevents infinite loop when addTab updates store state
|
||||
const storeAddTab = useViewerStore.getState().addTab;
|
||||
|
||||
// Get new executions that haven't been added yet
|
||||
const newExecutionIds = Object.keys(executions).filter(
|
||||
(id) => !addedExecutionsRef.current.has(id)
|
||||
);
|
||||
|
||||
if (newExecutionIds.length === 0) return;
|
||||
|
||||
// Distribute new executions across panes round-robin
|
||||
newExecutionIds.forEach((executionId, index) => {
|
||||
addedExecutionsRef.current.add(executionId);
|
||||
const exec = executions[executionId];
|
||||
const toolShort = exec.tool.split('-')[0];
|
||||
// Round-robin pane selection
|
||||
const targetPaneId = paneIds[index % paneIds.length];
|
||||
storeAddTab(targetPaneId, executionId, `${toolShort} (${exec.mode})`);
|
||||
});
|
||||
@@ -338,10 +247,7 @@ export function CliViewerPage() {
|
||||
useEffect(() => {
|
||||
const executionId = searchParams.get('executionId');
|
||||
if (executionId && focusedPaneId) {
|
||||
// Add tab to focused pane
|
||||
addTab(focusedPaneId, executionId, `Execution ${executionId.slice(0, 8)}`);
|
||||
|
||||
// Clear the URL param after processing
|
||||
setSearchParams((prev) => {
|
||||
const newParams = new URLSearchParams(prev);
|
||||
newParams.delete('executionId');
|
||||
@@ -350,104 +256,20 @@ export function CliViewerPage() {
|
||||
}
|
||||
}, [searchParams, focusedPaneId, addTab, setSearchParams]);
|
||||
|
||||
// Handle layout change
|
||||
const handleLayoutChange = useCallback(
|
||||
(layoutType: LayoutType) => {
|
||||
initializeDefaultLayout(layoutType);
|
||||
},
|
||||
[initializeDefaultLayout]
|
||||
);
|
||||
|
||||
// Handle reset
|
||||
const handleReset = useCallback(() => {
|
||||
reset();
|
||||
initializeDefaultLayout(DEFAULT_LAYOUT);
|
||||
}, [reset, initializeDefaultLayout]);
|
||||
|
||||
// Get current layout option for display
|
||||
const currentLayoutOption =
|
||||
LAYOUT_OPTIONS.find((l) => l.id === currentLayoutType) || LAYOUT_OPTIONS[1];
|
||||
const CurrentLayoutIcon = currentLayoutOption.icon;
|
||||
// Toggle fullscreen handler
|
||||
const handleToggleFullscreen = () => {
|
||||
setIsFullscreen((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* ======================================== */}
|
||||
{/* Toolbar */}
|
||||
{/* ======================================== */}
|
||||
<div className="flex items-center justify-between gap-3 p-3 bg-card border-b border-border">
|
||||
{/* Page Title */}
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Terminal className="w-5 h-5 text-primary flex-shrink-0" />
|
||||
<div className="flex flex-col min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliViewer.page.title' })}
|
||||
</span>
|
||||
{runningCount > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-green-500/10 text-green-600 dark:text-green-400 text-xs font-medium">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
|
||||
{runningCount} active
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage(
|
||||
{ id: 'cliViewer.page.subtitle' },
|
||||
{ count: activeSessionCount }
|
||||
)}
|
||||
{executionCount > 0 && ` · ${executionCount} executions`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Reset Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
title={formatMessage({ id: 'cliViewer.toolbar.clearAll' })}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* Layout Selector */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<CurrentLayoutIcon className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">
|
||||
{formatMessage({ id: currentLayoutOption.labelKey })}
|
||||
</span>
|
||||
<ChevronDown className="w-4 h-4 opacity-50" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>
|
||||
{formatMessage({ id: 'cliViewer.layout.title' })}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{LAYOUT_OPTIONS.map((option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={option.id}
|
||||
onClick={() => handleLayoutChange(option.id)}
|
||||
className={cn(
|
||||
'gap-2',
|
||||
currentLayoutType === option.id && 'bg-accent'
|
||||
)}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{formatMessage({ id: option.labelKey })}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<CliViewerToolbar
|
||||
isFullscreen={isFullscreen}
|
||||
onToggleFullscreen={handleToggleFullscreen}
|
||||
/>
|
||||
|
||||
{/* ======================================== */}
|
||||
{/* Layout Container */}
|
||||
|
||||
@@ -127,6 +127,7 @@ interface CliToolCardProps {
|
||||
onUpdateAvailableModels: (models: string[]) => void;
|
||||
onUpdateEnvFile: (envFile: string | undefined) => void;
|
||||
onUpdateSettingsFile: (settingsFile: string | undefined) => void;
|
||||
onUpdateEffort: (effort: string | undefined) => void;
|
||||
onSaveToBackend: () => void;
|
||||
}
|
||||
|
||||
@@ -145,6 +146,7 @@ function CliToolCard({
|
||||
onUpdateAvailableModels,
|
||||
onUpdateEnvFile,
|
||||
onUpdateSettingsFile,
|
||||
onUpdateEffort,
|
||||
onSaveToBackend,
|
||||
}: CliToolCardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
@@ -449,6 +451,39 @@ function CliToolCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Effort Level - for claude only */}
|
||||
{configFileType === 'settingsFile' && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.cliTools.effort' })}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{(['low', 'medium', 'high'] as const).map((level) => {
|
||||
const effectiveEffort = config.effort || 'high';
|
||||
const labelId = `settings.cliTools.effort${level.charAt(0).toUpperCase() + level.slice(1)}` as const;
|
||||
return (
|
||||
<button
|
||||
key={level}
|
||||
type="button"
|
||||
onClick={() => onUpdateEffort(level === 'high' && !config.effort ? undefined : level)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-md text-sm border transition-colors',
|
||||
effectiveEffort === level
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:bg-muted'
|
||||
)}
|
||||
>
|
||||
{formatMessage({ id: labelId })}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'settings.cliTools.effortHint' })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
{!isDefault && config.enabled && (
|
||||
@@ -948,6 +983,7 @@ interface CliToolsWithStatusProps {
|
||||
onUpdateAvailableModels: (toolId: string, models: string[]) => void;
|
||||
onUpdateEnvFile: (toolId: string, envFile: string | undefined) => void;
|
||||
onUpdateSettingsFile: (toolId: string, settingsFile: string | undefined) => void;
|
||||
onUpdateEffort: (toolId: string, effort: string | undefined) => void;
|
||||
onSaveToBackend: (toolId: string) => void;
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'];
|
||||
}
|
||||
@@ -965,6 +1001,7 @@ function CliToolsWithStatus({
|
||||
onUpdateAvailableModels,
|
||||
onUpdateEnvFile,
|
||||
onUpdateSettingsFile,
|
||||
onUpdateEffort,
|
||||
onSaveToBackend,
|
||||
formatMessage,
|
||||
}: CliToolsWithStatusProps) {
|
||||
@@ -995,6 +1032,7 @@ function CliToolsWithStatus({
|
||||
onUpdateAvailableModels={(models) => onUpdateAvailableModels(toolId, models)}
|
||||
onUpdateEnvFile={(envFile) => onUpdateEnvFile(toolId, envFile)}
|
||||
onUpdateSettingsFile={(settingsFile) => onUpdateSettingsFile(toolId, settingsFile)}
|
||||
onUpdateEffort={(effort) => onUpdateEffort(toolId, effort)}
|
||||
onSaveToBackend={() => onSaveToBackend(toolId)}
|
||||
/>
|
||||
);
|
||||
@@ -1057,6 +1095,10 @@ export function SettingsPage() {
|
||||
updateCliTool(toolId, { settingsFile });
|
||||
};
|
||||
|
||||
const handleUpdateEffort = (toolId: string, effort: string | undefined) => {
|
||||
updateCliTool(toolId, { effort });
|
||||
};
|
||||
|
||||
// Save tool config to backend (~/.claude/cli-tools.json)
|
||||
const handleSaveToBackend = useCallback(async (toolId: string) => {
|
||||
const config = cliTools[toolId];
|
||||
@@ -1078,6 +1120,7 @@ export function SettingsPage() {
|
||||
body.envFile = config.envFile || null;
|
||||
} else if (configFileType === 'settingsFile') {
|
||||
body.settingsFile = config.settingsFile || null;
|
||||
body.effort = config.effort || null;
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/cli/config/${toolId}`, {
|
||||
@@ -1210,6 +1253,7 @@ export function SettingsPage() {
|
||||
onUpdateAvailableModels={handleUpdateAvailableModels}
|
||||
onUpdateEnvFile={handleUpdateEnvFile}
|
||||
onUpdateSettingsFile={handleUpdateSettingsFile}
|
||||
onUpdateEffort={handleUpdateEffort}
|
||||
onSaveToBackend={handleSaveToBackend}
|
||||
formatMessage={formatMessage}
|
||||
/>
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { ChevronRight, Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { useFlowStore } from '@/stores';
|
||||
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||
import { useExecutionStore } from '@/stores/executionStore';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { FlowCanvas } from './FlowCanvas';
|
||||
|
||||
Reference in New Issue
Block a user