mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +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:
472
ccw/frontend/src/components/cli-viewer/CliViewerToolbar.tsx
Normal file
472
ccw/frontend/src/components/cli-viewer/CliViewerToolbar.tsx
Normal file
@@ -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<CliExecutionStatus, { color: string }> = {
|
||||
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 (
|
||||
<div className="flex items-center gap-1 px-2 h-[40px] border-b border-border bg-muted/30 shrink-0">
|
||||
{/* Back button */}
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className={cn(
|
||||
'p-1.5 rounded transition-colors',
|
||||
'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
)}
|
||||
title={formatMessage({ id: 'cliViewer.toolbar.back', defaultMessage: 'Back' })}
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-px h-5 bg-border mx-1" />
|
||||
|
||||
{/* Layout presets */}
|
||||
{LAYOUT_PRESETS.map((preset) => {
|
||||
const isActive = currentLayoutType === preset.id;
|
||||
return (
|
||||
<button
|
||||
key={preset.id}
|
||||
onClick={() => handleLayoutChange(preset.id)}
|
||||
className={cn(
|
||||
'p-1.5 rounded transition-colors',
|
||||
isActive
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
)}
|
||||
title={formatMessage({ id: preset.labelId })}
|
||||
>
|
||||
<preset.icon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-px h-5 bg-border mx-1" />
|
||||
|
||||
{/* Add execution button - Inline Picker */}
|
||||
<AddExecutionButton focusedPaneId={focusedPaneId} />
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-px h-5 bg-border mx-1" />
|
||||
|
||||
{/* Reset button */}
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className={cn(
|
||||
'p-1.5 rounded transition-colors',
|
||||
'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
)}
|
||||
title={formatMessage({ id: 'cliViewer.toolbar.clearAll' })}
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
{/* Right side - Execution selector & fullscreen */}
|
||||
<div className="flex items-center gap-1 ml-auto">
|
||||
{/* Execution dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs transition-colors',
|
||||
'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
)}
|
||||
>
|
||||
<Terminal className="w-3.5 h-3.5" />
|
||||
<span>
|
||||
{runningCount > 0
|
||||
? `${runningCount} ${formatMessage({ id: 'cliViewer.toolbar.running', defaultMessage: 'running' })}`
|
||||
: `${executionCount} ${formatMessage({ id: 'cliViewer.toolbar.executions', defaultMessage: 'executions' })}`}
|
||||
</span>
|
||||
{runningCount > 0 && (
|
||||
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||
)}
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" sideOffset={4}>
|
||||
<DropdownMenuLabel>
|
||||
{formatMessage({ id: 'cliViewer.toolbar.executionsList', defaultMessage: 'Recent Executions' })}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{Object.entries(executions).length === 0 ? (
|
||||
<div className="px-2 py-4 text-center text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'cliViewer.picker.noExecutions', defaultMessage: 'No executions available' })}
|
||||
</div>
|
||||
) : (
|
||||
Object.entries(executions)
|
||||
.sort((a, b) => b[1].startTime - a[1].startTime)
|
||||
.slice(0, 10)
|
||||
.map(([id, exec]) => (
|
||||
<DropdownMenuItem
|
||||
key={id}
|
||||
className="flex items-center gap-2 text-xs"
|
||||
onClick={() => {
|
||||
if (focusedPaneId) {
|
||||
const title = `${exec.tool}-${exec.mode}`;
|
||||
addTab(focusedPaneId, id, title);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'w-2 h-2 rounded-full shrink-0',
|
||||
STATUS_CONFIG[exec.status].color
|
||||
)}
|
||||
/>
|
||||
<span className="truncate flex-1">{exec.tool}</span>
|
||||
<span className="text-muted-foreground">{exec.mode}</span>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-px h-5 bg-border mx-1" />
|
||||
|
||||
{/* Fullscreen toggle */}
|
||||
<button
|
||||
onClick={onToggleFullscreen}
|
||||
className={cn(
|
||||
'p-1.5 rounded transition-colors',
|
||||
isFullscreen
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
)}
|
||||
title={
|
||||
isFullscreen
|
||||
? formatMessage({ id: 'cliViewer.toolbar.exitFullscreen', defaultMessage: 'Exit Fullscreen' })
|
||||
: formatMessage({ id: 'cliViewer.toolbar.fullscreen', defaultMessage: 'Fullscreen' })
|
||||
}
|
||||
>
|
||||
{isFullscreen ? (
|
||||
<Minimize2 className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<Maximize2 className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Page title */}
|
||||
<span className="text-xs text-muted-foreground font-medium ml-2">
|
||||
{formatMessage({ id: 'cliViewer.page.title' })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== 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<string>();
|
||||
const pane = panes[focusedPaneId];
|
||||
if (!pane) return new Set<string>();
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs transition-colors',
|
||||
'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
)}
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>{formatMessage({ id: 'cliViewer.toolbar.addExecution', defaultMessage: 'Add' })}</span>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{formatMessage({ id: 'cliViewer.picker.selectExecution', defaultMessage: 'Select Execution' })}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Search input */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={formatMessage({
|
||||
id: 'cliViewer.picker.searchExecutions',
|
||||
defaultMessage: 'Search executions...'
|
||||
})}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Execution list */}
|
||||
<div className="max-h-[300px] overflow-y-auto space-y-2">
|
||||
{filteredExecutions.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<Terminal className="h-8 w-8 text-muted-foreground mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{Object.keys(executions).length === 0
|
||||
? formatMessage({ id: 'cliViewer.picker.noExecutions', defaultMessage: 'No executions available' })
|
||||
: formatMessage({ id: 'cliViewer.picker.noMatchingExecutions', defaultMessage: 'No matching executions' })}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredExecutions.map(([id, exec]) => {
|
||||
const isAlreadyOpen = existingExecutionIds.has(id);
|
||||
return (
|
||||
<div key={id} className="relative">
|
||||
<button
|
||||
onClick={() => handleSelect(id, exec.tool, exec.mode)}
|
||||
disabled={isAlreadyOpen}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 p-3 rounded-lg',
|
||||
'border border-border/50 bg-muted/30',
|
||||
'hover:bg-muted/50 hover:border-border',
|
||||
'transition-all duration-150',
|
||||
'text-left',
|
||||
isAlreadyOpen && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{/* Tool icon */}
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-md bg-primary/10">
|
||||
<Terminal className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
|
||||
{/* Execution info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm text-foreground truncate">
|
||||
{exec.tool}-{exec.mode}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'w-2 h-2 rounded-full shrink-0',
|
||||
STATUS_CONFIG[exec.status].color
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{id}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Time */}
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground shrink-0">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{formatTime(exec.startTime)}</span>
|
||||
</div>
|
||||
</button>
|
||||
{isAlreadyOpen && (
|
||||
<div className="absolute inset-0 bg-background/60 rounded-lg flex items-center justify-center">
|
||||
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
|
||||
{formatMessage({ id: 'cliViewer.picker.alreadyOpen', defaultMessage: 'Already open' })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default CliViewerToolbar;
|
||||
@@ -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';
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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<CliTool>('gemini');
|
||||
|
||||
@@ -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({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={toggleShowHidden}
|
||||
title={formatMessage({ id: 'terminalDashboard.fileBrowser.showHidden' })}
|
||||
>
|
||||
{state.showHiddenFiles ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Copy, ArrowRightToLine, Loader2, RefreshCw } from 'lucide-react';
|
||||
import { Copy, ArrowRightToLine, Loader2, RefreshCw, Eye, EyeOff } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { FloatingPanel } from './FloatingPanel';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -42,6 +42,7 @@ export function FloatingFileBrowser({
|
||||
refetch,
|
||||
setSelectedFile,
|
||||
toggleExpanded,
|
||||
toggleShowHidden,
|
||||
} = useFileExplorer({
|
||||
rootPath,
|
||||
maxDepth: 6,
|
||||
@@ -107,6 +108,17 @@ export function FloatingFileBrowser({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={toggleShowHidden}
|
||||
title={formatMessage({ id: 'terminalDashboard.fileBrowser.showHidden' })}
|
||||
>
|
||||
{state.showHiddenFiles ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
} from '@/stores/issueQueueIntegrationStore';
|
||||
import { useCliSessionStore } from '@/stores/cliSessionStore';
|
||||
import { getAllPaneIds } from '@/lib/layout-utils';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { useFileContent } from '@/hooks/useFileExplorer';
|
||||
import type { PaneId } from '@/stores/viewerStore';
|
||||
import type { TerminalStatus } from '@/types/terminal-dashboard';
|
||||
@@ -86,8 +85,6 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
||||
const canClose = getAllPaneIds(layout).length > 1;
|
||||
const isFileMode = displayMode === 'file' && filePath;
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
// Session data
|
||||
const groups = useSessionManagerStore(selectGroups);
|
||||
const terminalMetas = useSessionManagerStore(selectTerminalMetas);
|
||||
|
||||
Reference in New Issue
Block a user