From c67bf86244d69a93b98756d2fdaea9d16da9e0fa Mon Sep 17 00:00:00 2001 From: catlog22 Date: Tue, 17 Feb 2026 20:02:44 +0800 Subject: [PATCH] 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. --- .../cli-viewer/CliViewerToolbar.tsx | 472 ++++++++++++++++++ .../src/components/cli-viewer/index.ts | 4 + .../issue/queue/ExecutionGroup.test.tsx | 2 +- .../terminal-dashboard/DashboardToolbar.tsx | 2 +- .../terminal-dashboard/FileSidebarPanel.tsx | 12 +- .../FloatingFileBrowser.tsx | 14 +- .../terminal-dashboard/TerminalPane.tsx | 3 - ccw/frontend/src/hooks/useFileExplorer.ts | 8 +- ccw/frontend/src/lib/layout-utils.test.ts | 2 +- ccw/frontend/src/locales/en/cli-viewer.json | 9 +- ccw/frontend/src/locales/en/settings.json | 7 +- .../src/locales/en/terminal-dashboard.json | 16 +- ccw/frontend/src/locales/zh/settings.json | 7 +- .../src/locales/zh/terminal-dashboard.json | 16 +- ccw/frontend/src/pages/CliViewerPage.tsx | 210 +------- ccw/frontend/src/pages/SettingsPage.tsx | 44 ++ .../pages/orchestrator/OrchestratorPage.tsx | 3 +- ccw/frontend/src/types/store.ts | 2 + ccw/src/cli.ts | 2 + ccw/src/commands/cli.ts | 7 +- ccw/src/core/routes/cli-routes.ts | 2 +- ccw/src/core/routes/files-routes.ts | 31 +- ccw/src/tools/ask-question.ts | 18 +- ccw/src/tools/claude-cli-tools.ts | 19 +- ccw/src/tools/cli-config-manager.ts | 4 +- ccw/src/tools/cli-executor-core.ts | 13 +- ccw/src/tools/cli-executor-utils.ts | 8 +- 27 files changed, 696 insertions(+), 241 deletions(-) create mode 100644 ccw/frontend/src/components/cli-viewer/CliViewerToolbar.tsx diff --git a/ccw/frontend/src/components/cli-viewer/CliViewerToolbar.tsx b/ccw/frontend/src/components/cli-viewer/CliViewerToolbar.tsx new file mode 100644 index 00000000..87ee4879 --- /dev/null +++ b/ccw/frontend/src/components/cli-viewer/CliViewerToolbar.tsx @@ -0,0 +1,472 @@ +// ======================================== +// CliViewerToolbar Component +// ======================================== +// Compact icon-based toolbar for CLI Viewer page +// Follows DashboardToolbar design pattern + +import { useCallback, useMemo, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { useNavigate } from 'react-router-dom'; +import { + ArrowLeft, + Square, + Columns2, + Rows2, + LayoutGrid, + Plus, + ChevronDown, + Maximize2, + Minimize2, + RotateCcw, + Terminal, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/Dropdown'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/Dialog'; +import { Input } from '@/components/ui/Input'; +import { Search, Clock, CheckCircle2, XCircle, Loader2 } from 'lucide-react'; +import { + useViewerStore, + useViewerLayout, + useFocusedPaneId, + type AllotmentLayout, +} from '@/stores/viewerStore'; +import { useCliStreamStore, type CliExecutionStatus } from '@/stores/cliStreamStore'; + +// ========== Types ========== + +export interface CliViewerToolbarProps { + /** Whether fullscreen mode is active */ + isFullscreen?: boolean; + /** Callback to toggle fullscreen mode */ + onToggleFullscreen?: () => void; +} + +export type LayoutType = 'single' | 'split-h' | 'split-v' | 'grid-2x2'; + +// ========== Constants ========== + +const LAYOUT_PRESETS = [ + { id: 'single' as const, icon: Square, labelId: 'cliViewer.layout.single' }, + { id: 'split-h' as const, icon: Columns2, labelId: 'cliViewer.layout.splitH' }, + { id: 'split-v' as const, icon: Rows2, labelId: 'cliViewer.layout.splitV' }, + { id: 'grid-2x2' as const, icon: LayoutGrid, labelId: 'cliViewer.layout.grid' }, +]; + +const DEFAULT_LAYOUT: LayoutType = 'split-h'; + +const STATUS_CONFIG: Record = { + running: { color: 'bg-blue-500 animate-pulse' }, + completed: { color: 'bg-green-500' }, + error: { color: 'bg-red-500' }, +}; + +// ========== Helper Functions ========== + +/** + * Detect layout type from AllotmentLayout structure + */ +function detectLayoutType(layout: AllotmentLayout): LayoutType { + const childCount = layout.children.length; + + if (childCount === 0 || childCount === 1) { + return 'single'; + } + + if (childCount === 2) { + const hasNestedGroups = layout.children.some( + (child) => typeof child !== 'string' + ); + + if (!hasNestedGroups) { + return layout.direction === 'horizontal' ? 'split-h' : 'split-v'; + } + + const allNested = layout.children.every( + (child) => typeof child !== 'string' + ); + if (allNested) { + return 'grid-2x2'; + } + } + + return layout.direction === 'horizontal' ? 'split-h' : 'split-v'; +} + +function formatTime(timestamp: number): string { + const now = Date.now(); + const diff = now - timestamp; + + if (diff < 60000) return 'Just now'; + if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; + if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; + return new Date(timestamp).toLocaleDateString(); +} + +// ========== Component ========== + +export function CliViewerToolbar({ + isFullscreen, + onToggleFullscreen, +}: CliViewerToolbarProps) { + const { formatMessage } = useIntl(); + const navigate = useNavigate(); + + // Store hooks + const layout = useViewerLayout(); + const focusedPaneId = useFocusedPaneId(); + const { initializeDefaultLayout, reset, addTab } = useViewerStore(); + + // CLI Stream Store + const executions = useCliStreamStore((state) => state.executions); + + // Detect current layout type + const currentLayoutType = useMemo(() => detectLayoutType(layout), [layout]); + + // 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 back navigation + const handleBack = useCallback(() => { + navigate(-1); + }, [navigate]); + + // Handle layout change + const handleLayoutChange = useCallback( + (layoutType: LayoutType) => { + initializeDefaultLayout(layoutType); + }, + [initializeDefaultLayout] + ); + + // Handle reset + const handleReset = useCallback(() => { + reset(); + initializeDefaultLayout(DEFAULT_LAYOUT); + }, [reset, initializeDefaultLayout]); + + return ( +
+ {/* Back button */} + + + {/* Separator */} +
+ + {/* Layout presets */} + {LAYOUT_PRESETS.map((preset) => { + const isActive = currentLayoutType === preset.id; + return ( + + ); + })} + + {/* Separator */} +
+ + {/* Add execution button - Inline Picker */} + + + {/* Separator */} +
+ + {/* Reset button */} + + + {/* Right side - Execution selector & fullscreen */} +
+ {/* Execution dropdown */} + + + + + + + {formatMessage({ id: 'cliViewer.toolbar.executionsList', defaultMessage: 'Recent Executions' })} + + + {Object.entries(executions).length === 0 ? ( +
+ {formatMessage({ id: 'cliViewer.picker.noExecutions', defaultMessage: 'No executions available' })} +
+ ) : ( + Object.entries(executions) + .sort((a, b) => b[1].startTime - a[1].startTime) + .slice(0, 10) + .map(([id, exec]) => ( + { + if (focusedPaneId) { + const title = `${exec.tool}-${exec.mode}`; + addTab(focusedPaneId, id, title); + } + }} + > + + {exec.tool} + {exec.mode} + + )) + )} +
+
+ + {/* Separator */} +
+ + {/* Fullscreen toggle */} + + + {/* Page title */} + + {formatMessage({ id: 'cliViewer.page.title' })} + +
+
+ ); +} + +// ========== Add Execution Button Sub-Component ========== + +function AddExecutionButton({ focusedPaneId }: { focusedPaneId: string | null }) { + const { formatMessage } = useIntl(); + const [open, setOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + const executions = useCliStreamStore((state) => state.executions); + const panes = useViewerStore((state) => state.panes); + const addTab = useViewerStore((state) => state.addTab); + + // Get existing execution IDs in current pane + const existingExecutionIds = useMemo(() => { + if (!focusedPaneId) return new Set(); + const pane = panes[focusedPaneId]; + if (!pane) return new Set(); + return new Set(pane.tabs.map((tab) => tab.executionId)); + }, [panes, focusedPaneId]); + + // Filter executions + const filteredExecutions = useMemo(() => { + const entries = Object.entries(executions); + const filtered = entries.filter(([id, exec]) => { + if (!searchQuery) return true; + const query = searchQuery.toLowerCase(); + return ( + id.toLowerCase().includes(query) || + exec.tool.toLowerCase().includes(query) || + exec.mode.toLowerCase().includes(query) + ); + }); + filtered.sort((a, b) => b[1].startTime - a[1].startTime); + return filtered; + }, [executions, searchQuery]); + + const handleSelect = useCallback((executionId: string, tool: string, mode: string) => { + if (focusedPaneId) { + addTab(focusedPaneId, executionId, `${tool}-${mode}`); + setOpen(false); + setSearchQuery(''); + } + }, [focusedPaneId, addTab]); + + if (!focusedPaneId) return null; + + return ( + + + + + + + + + {formatMessage({ id: 'cliViewer.picker.selectExecution', defaultMessage: 'Select Execution' })} + + + + {/* Search input */} +
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+ + {/* Execution list */} +
+ {filteredExecutions.length === 0 ? ( +
+ +

+ {Object.keys(executions).length === 0 + ? formatMessage({ id: 'cliViewer.picker.noExecutions', defaultMessage: 'No executions available' }) + : formatMessage({ id: 'cliViewer.picker.noMatchingExecutions', defaultMessage: 'No matching executions' })} +

+
+ ) : ( + filteredExecutions.map(([id, exec]) => { + const isAlreadyOpen = existingExecutionIds.has(id); + return ( +
+ + {isAlreadyOpen && ( +
+ + {formatMessage({ id: 'cliViewer.picker.alreadyOpen', defaultMessage: 'Already open' })} + +
+ )} +
+ ); + }) + )} +
+
+
+ ); +} + +export default CliViewerToolbar; diff --git a/ccw/frontend/src/components/cli-viewer/index.ts b/ccw/frontend/src/components/cli-viewer/index.ts index 90e71820..dedd1693 100644 --- a/ccw/frontend/src/components/cli-viewer/index.ts +++ b/ccw/frontend/src/components/cli-viewer/index.ts @@ -26,3 +26,7 @@ export type { ContentAreaProps } from './ContentArea'; // Empty state export { EmptyState } from './EmptyState'; export type { EmptyStateProps } from './EmptyState'; + +// Toolbar +export { CliViewerToolbar } from './CliViewerToolbar'; +export type { CliViewerToolbarProps, LayoutType } from './CliViewerToolbar'; diff --git a/ccw/frontend/src/components/issue/queue/ExecutionGroup.test.tsx b/ccw/frontend/src/components/issue/queue/ExecutionGroup.test.tsx index cacdec85..2852ecfd 100644 --- a/ccw/frontend/src/components/issue/queue/ExecutionGroup.test.tsx +++ b/ccw/frontend/src/components/issue/queue/ExecutionGroup.test.tsx @@ -118,8 +118,8 @@ describe('ExecutionGroup', () => { } // After expand, items should be visible - const expandedContainer = document.querySelector('.space-y-1.mt-2'); // Note: This test verifies the click handler works; state change verification + // eslint-disable-next-line @typescript-eslint/no-unused-vars }); it('should be clickable via header', () => { diff --git a/ccw/frontend/src/components/terminal-dashboard/DashboardToolbar.tsx b/ccw/frontend/src/components/terminal-dashboard/DashboardToolbar.tsx index ac9444e6..e0abe7d3 100644 --- a/ccw/frontend/src/components/terminal-dashboard/DashboardToolbar.tsx +++ b/ccw/frontend/src/components/terminal-dashboard/DashboardToolbar.tsx @@ -128,7 +128,7 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen // Launch CLI handlers const projectPath = useWorkflowStore(selectProjectPath); const focusedPaneId = useTerminalGridStore(selectTerminalGridFocusedPaneId); - const panes = useTerminalGridStore((s) => s.panes); + // panes available via: useTerminalGridStore((s) => s.panes) const createSessionAndAssign = useTerminalGridStore((s) => s.createSessionAndAssign); const [isCreating, setIsCreating] = useState(false); const [selectedTool, setSelectedTool] = useState('gemini'); diff --git a/ccw/frontend/src/components/terminal-dashboard/FileSidebarPanel.tsx b/ccw/frontend/src/components/terminal-dashboard/FileSidebarPanel.tsx index 33277e72..ebe0c30c 100644 --- a/ccw/frontend/src/components/terminal-dashboard/FileSidebarPanel.tsx +++ b/ccw/frontend/src/components/terminal-dashboard/FileSidebarPanel.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { useIntl } from 'react-intl'; -import { FolderOpen, RefreshCw, Loader2, ChevronLeft, FileText } from 'lucide-react'; +import { FolderOpen, RefreshCw, Loader2, ChevronLeft, FileText, Eye, EyeOff } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/Button'; import { TreeView } from '@/components/shared/TreeView'; @@ -50,6 +50,7 @@ export function FileSidebarPanel({ refetch, setSelectedFile, toggleExpanded, + toggleShowHidden, } = useFileExplorer({ rootPath, maxDepth: 6, @@ -139,6 +140,15 @@ export function FileSidebarPanel({ )}
+
+ + - - {/* Layout Selector */} - - - - - - - {formatMessage({ id: 'cliViewer.layout.title' })} - - - {LAYOUT_OPTIONS.map((option) => { - const Icon = option.icon; - return ( - handleLayoutChange(option.id)} - className={cn( - 'gap-2', - currentLayoutType === option.id && 'bg-accent' - )} - > - - {formatMessage({ id: option.labelKey })} - - ); - })} - - -
-
+ {/* ======================================== */} {/* Layout Container */} diff --git a/ccw/frontend/src/pages/SettingsPage.tsx b/ccw/frontend/src/pages/SettingsPage.tsx index 6002fc27..d77b7ff4 100644 --- a/ccw/frontend/src/pages/SettingsPage.tsx +++ b/ccw/frontend/src/pages/SettingsPage.tsx @@ -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({
)} + {/* Effort Level - for claude only */} + {configFileType === 'settingsFile' && ( +
+ +
+ {(['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 ( + + ); + })} +
+

+ {formatMessage({ id: 'settings.cliTools.effortHint' })} +

+
+ )} + {/* Action Buttons */}
{!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['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} /> diff --git a/ccw/frontend/src/pages/orchestrator/OrchestratorPage.tsx b/ccw/frontend/src/pages/orchestrator/OrchestratorPage.tsx index 006964f6..c15e1be4 100644 --- a/ccw/frontend/src/pages/orchestrator/OrchestratorPage.tsx +++ b/ccw/frontend/src/pages/orchestrator/OrchestratorPage.tsx @@ -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'; diff --git a/ccw/frontend/src/types/store.ts b/ccw/frontend/src/types/store.ts index d09ba29a..b6e0f778 100644 --- a/ccw/frontend/src/types/store.ts +++ b/ccw/frontend/src/types/store.ts @@ -396,6 +396,8 @@ export interface CliToolConfig { /** Path to Claude CLI settings.json, passed via --settings (claude only) */ settingsFile?: string; availableModels?: string[]; + /** Default effort level for claude (low, medium, high) */ + effort?: string; } export interface ApiEndpoints { diff --git a/ccw/src/cli.ts b/ccw/src/cli.ts index 424b050d..00a244d0 100644 --- a/ccw/src/cli.ts +++ b/ccw/src/cli.ts @@ -192,6 +192,8 @@ export function run(argv: string[]): void { .option('--inject-mode ', 'Inject mode: none, full, progressive (default: codex=full, others=none)') // Template/Rules options .option('--rule