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
|
// Empty state
|
||||||
export { EmptyState } from './EmptyState';
|
export { EmptyState } from './EmptyState';
|
||||||
export type { EmptyStateProps } 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
|
// 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
|
// 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', () => {
|
it('should be clickable via header', () => {
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
|||||||
// Launch CLI handlers
|
// Launch CLI handlers
|
||||||
const projectPath = useWorkflowStore(selectProjectPath);
|
const projectPath = useWorkflowStore(selectProjectPath);
|
||||||
const focusedPaneId = useTerminalGridStore(selectTerminalGridFocusedPaneId);
|
const focusedPaneId = useTerminalGridStore(selectTerminalGridFocusedPaneId);
|
||||||
const panes = useTerminalGridStore((s) => s.panes);
|
// panes available via: useTerminalGridStore((s) => s.panes)
|
||||||
const createSessionAndAssign = useTerminalGridStore((s) => s.createSessionAndAssign);
|
const createSessionAndAssign = useTerminalGridStore((s) => s.createSessionAndAssign);
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [selectedTool, setSelectedTool] = useState<CliTool>('gemini');
|
const [selectedTool, setSelectedTool] = useState<CliTool>('gemini');
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
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 { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { TreeView } from '@/components/shared/TreeView';
|
import { TreeView } from '@/components/shared/TreeView';
|
||||||
@@ -50,6 +50,7 @@ export function FileSidebarPanel({
|
|||||||
refetch,
|
refetch,
|
||||||
setSelectedFile,
|
setSelectedFile,
|
||||||
toggleExpanded,
|
toggleExpanded,
|
||||||
|
toggleShowHidden,
|
||||||
} = useFileExplorer({
|
} = useFileExplorer({
|
||||||
rootPath,
|
rootPath,
|
||||||
maxDepth: 6,
|
maxDepth: 6,
|
||||||
@@ -139,6 +140,15 @@ export function FileSidebarPanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
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 { cn } from '@/lib/utils';
|
||||||
import { FloatingPanel } from './FloatingPanel';
|
import { FloatingPanel } from './FloatingPanel';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
@@ -42,6 +42,7 @@ export function FloatingFileBrowser({
|
|||||||
refetch,
|
refetch,
|
||||||
setSelectedFile,
|
setSelectedFile,
|
||||||
toggleExpanded,
|
toggleExpanded,
|
||||||
|
toggleShowHidden,
|
||||||
} = useFileExplorer({
|
} = useFileExplorer({
|
||||||
rootPath,
|
rootPath,
|
||||||
maxDepth: 6,
|
maxDepth: 6,
|
||||||
@@ -107,6 +108,17 @@ export function FloatingFileBrowser({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import {
|
|||||||
} from '@/stores/issueQueueIntegrationStore';
|
} from '@/stores/issueQueueIntegrationStore';
|
||||||
import { useCliSessionStore } from '@/stores/cliSessionStore';
|
import { useCliSessionStore } from '@/stores/cliSessionStore';
|
||||||
import { getAllPaneIds } from '@/lib/layout-utils';
|
import { getAllPaneIds } from '@/lib/layout-utils';
|
||||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
|
||||||
import { useFileContent } from '@/hooks/useFileExplorer';
|
import { useFileContent } from '@/hooks/useFileExplorer';
|
||||||
import type { PaneId } from '@/stores/viewerStore';
|
import type { PaneId } from '@/stores/viewerStore';
|
||||||
import type { TerminalStatus } from '@/types/terminal-dashboard';
|
import type { TerminalStatus } from '@/types/terminal-dashboard';
|
||||||
@@ -86,8 +85,6 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
const canClose = getAllPaneIds(layout).length > 1;
|
const canClose = getAllPaneIds(layout).length > 1;
|
||||||
const isFileMode = displayMode === 'file' && filePath;
|
const isFileMode = displayMode === 'file' && filePath;
|
||||||
|
|
||||||
const projectPath = useWorkflowStore(selectProjectPath);
|
|
||||||
|
|
||||||
// Session data
|
// Session data
|
||||||
const groups = useSessionManagerStore(selectGroups);
|
const groups = useSessionManagerStore(selectGroups);
|
||||||
const terminalMetas = useSessionManagerStore(selectTerminalMetas);
|
const terminalMetas = useSessionManagerStore(selectTerminalMetas);
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export function useFileExplorer(options: UseFileExplorerOptions = {}): UseFileEx
|
|||||||
const {
|
const {
|
||||||
rootPath = '/',
|
rootPath = '/',
|
||||||
maxDepth = 5,
|
maxDepth = 5,
|
||||||
includeHidden = false,
|
// includeHidden is now controlled by internal showHiddenFiles state
|
||||||
excludePatterns,
|
excludePatterns,
|
||||||
staleTime,
|
staleTime,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
@@ -126,10 +126,10 @@ export function useFileExplorer(options: UseFileExplorerOptions = {}): UseFileEx
|
|||||||
const [filter, setFilterState] = useState('');
|
const [filter, setFilterState] = useState('');
|
||||||
const [searchResults, setSearchResults] = useState<SearchFilesResponse | undefined>();
|
const [searchResults, setSearchResults] = useState<SearchFilesResponse | undefined>();
|
||||||
|
|
||||||
// Fetch file tree
|
// Fetch file tree - use showHiddenFiles state instead of options.includeHidden
|
||||||
const treeQuery = useQuery({
|
const treeQuery = useQuery({
|
||||||
queryKey: fileExplorerKeys.tree(rootPath),
|
queryKey: [...fileExplorerKeys.tree(rootPath), { showHidden: showHiddenFiles }],
|
||||||
queryFn: () => fetchFileTree(rootPath, { maxDepth, includeHidden, excludePatterns }),
|
queryFn: () => fetchFileTree(rootPath, { maxDepth, includeHidden: showHiddenFiles, excludePatterns }),
|
||||||
staleTime: staleTime ?? TREE_STALE_TIME,
|
staleTime: staleTime ?? TREE_STALE_TIME,
|
||||||
enabled,
|
enabled,
|
||||||
retry: 2,
|
retry: 2,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
addPaneToLayout,
|
addPaneToLayout,
|
||||||
getAllPaneIds,
|
getAllPaneIds,
|
||||||
} from './layout-utils';
|
} from './layout-utils';
|
||||||
import type { AllotmentLayoutGroup, PaneId } from '@/stores/viewerStore';
|
import type { AllotmentLayoutGroup } from '@/stores/viewerStore';
|
||||||
|
|
||||||
describe('layout-utils', () => {
|
describe('layout-utils', () => {
|
||||||
// Helper to create test layouts
|
// Helper to create test layouts
|
||||||
|
|||||||
@@ -21,7 +21,14 @@
|
|||||||
"toolbar": {
|
"toolbar": {
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"clearAll": "Clear All",
|
"clearAll": "Clear All",
|
||||||
"settings": "Settings"
|
"settings": "Settings",
|
||||||
|
"back": "Back",
|
||||||
|
"addExecution": "Add",
|
||||||
|
"running": "running",
|
||||||
|
"executions": "executions",
|
||||||
|
"executionsList": "Recent Executions",
|
||||||
|
"fullscreen": "Fullscreen",
|
||||||
|
"exitFullscreen": "Exit Fullscreen"
|
||||||
},
|
},
|
||||||
"emptyState": {
|
"emptyState": {
|
||||||
"title": "No CLI Executions",
|
"title": "No CLI Executions",
|
||||||
|
|||||||
@@ -41,7 +41,12 @@
|
|||||||
"saveToConfig": "Save to Config",
|
"saveToConfig": "Save to Config",
|
||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
"configSaved": "Configuration saved to ~/.claude/cli-tools.json",
|
"configSaved": "Configuration saved to ~/.claude/cli-tools.json",
|
||||||
"configSaveError": "Failed to save configuration"
|
"configSaveError": "Failed to save configuration",
|
||||||
|
"effort": "Effort Level",
|
||||||
|
"effortHint": "Controls thinking effort for Claude sessions. Default: high.",
|
||||||
|
"effortLow": "Low",
|
||||||
|
"effortMedium": "Medium",
|
||||||
|
"effortHigh": "High"
|
||||||
},
|
},
|
||||||
"display": {
|
"display": {
|
||||||
"title": "Display Settings",
|
"title": "Display Settings",
|
||||||
|
|||||||
@@ -84,7 +84,8 @@
|
|||||||
"modeDefault": "Default",
|
"modeDefault": "Default",
|
||||||
"modeYolo": "Yolo",
|
"modeYolo": "Yolo",
|
||||||
"quickCreate": "Quick Create",
|
"quickCreate": "Quick Create",
|
||||||
"configure": "Configure..."
|
"configure": "Configure...",
|
||||||
|
"fullscreen": "Fullscreen"
|
||||||
},
|
},
|
||||||
"cliConfig": {
|
"cliConfig": {
|
||||||
"title": "Create CLI Session",
|
"title": "Create CLI Session",
|
||||||
@@ -113,7 +114,15 @@
|
|||||||
"copied": "Copied",
|
"copied": "Copied",
|
||||||
"insertPath": "Insert into terminal",
|
"insertPath": "Insert into terminal",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"loadFailed": "Failed to load file tree"
|
"loadFailed": "Failed to load file tree",
|
||||||
|
"showHidden": "Toggle filtered files"
|
||||||
|
},
|
||||||
|
"fileSidebar": {
|
||||||
|
"title": "Files",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"noProject": "No project open",
|
||||||
|
"openProjectHint": "Open a project to browse files"
|
||||||
},
|
},
|
||||||
"artifacts": {
|
"artifacts": {
|
||||||
"types": {
|
"types": {
|
||||||
@@ -134,7 +143,8 @@
|
|||||||
"linkedIssue": "Linked Issue",
|
"linkedIssue": "Linked Issue",
|
||||||
"restart": "Restart Session",
|
"restart": "Restart Session",
|
||||||
"pause": "Pause Session",
|
"pause": "Pause Session",
|
||||||
"resume": "Resume Session"
|
"resume": "Resume Session",
|
||||||
|
"backToTerminal": "Back to terminal"
|
||||||
},
|
},
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
"noTabs": "No terminal sessions"
|
"noTabs": "No terminal sessions"
|
||||||
|
|||||||
@@ -41,7 +41,12 @@
|
|||||||
"saveToConfig": "保存到配置文件",
|
"saveToConfig": "保存到配置文件",
|
||||||
"saving": "保存中...",
|
"saving": "保存中...",
|
||||||
"configSaved": "配置已保存到 ~/.claude/cli-tools.json",
|
"configSaved": "配置已保存到 ~/.claude/cli-tools.json",
|
||||||
"configSaveError": "保存配置失败"
|
"configSaveError": "保存配置失败",
|
||||||
|
"effort": "思考力度",
|
||||||
|
"effortHint": "控制 Claude 会话的思考力度。默认:high。",
|
||||||
|
"effortLow": "低",
|
||||||
|
"effortMedium": "中",
|
||||||
|
"effortHigh": "高"
|
||||||
},
|
},
|
||||||
"display": {
|
"display": {
|
||||||
"title": "显示设置",
|
"title": "显示设置",
|
||||||
|
|||||||
@@ -84,7 +84,8 @@
|
|||||||
"modeDefault": "默认",
|
"modeDefault": "默认",
|
||||||
"modeYolo": "Yolo",
|
"modeYolo": "Yolo",
|
||||||
"quickCreate": "快速创建",
|
"quickCreate": "快速创建",
|
||||||
"configure": "配置..."
|
"configure": "配置...",
|
||||||
|
"fullscreen": "全屏"
|
||||||
},
|
},
|
||||||
"cliConfig": {
|
"cliConfig": {
|
||||||
"title": "创建 CLI 会话",
|
"title": "创建 CLI 会话",
|
||||||
@@ -113,7 +114,15 @@
|
|||||||
"copied": "已复制",
|
"copied": "已复制",
|
||||||
"insertPath": "插入到终端",
|
"insertPath": "插入到终端",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"loadFailed": "加载文件树失败"
|
"loadFailed": "加载文件树失败",
|
||||||
|
"showHidden": "显示/隐藏过滤文件"
|
||||||
|
},
|
||||||
|
"fileSidebar": {
|
||||||
|
"title": "文件",
|
||||||
|
"refresh": "刷新",
|
||||||
|
"collapse": "折叠",
|
||||||
|
"noProject": "未打开项目",
|
||||||
|
"openProjectHint": "打开项目以浏览文件"
|
||||||
},
|
},
|
||||||
"artifacts": {
|
"artifacts": {
|
||||||
"types": {
|
"types": {
|
||||||
@@ -134,7 +143,8 @@
|
|||||||
"linkedIssue": "关联问题",
|
"linkedIssue": "关联问题",
|
||||||
"restart": "重启会话",
|
"restart": "重启会话",
|
||||||
"pause": "暂停会话",
|
"pause": "暂停会话",
|
||||||
"resume": "恢复会话"
|
"resume": "恢复会话",
|
||||||
|
"backToTerminal": "返回终端"
|
||||||
},
|
},
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
"noTabs": "暂无终端会话"
|
"noTabs": "暂无终端会话"
|
||||||
|
|||||||
@@ -5,29 +5,9 @@
|
|||||||
// Integrates with viewerStore for state management
|
// Integrates with viewerStore for state management
|
||||||
// Includes WebSocket integration and execution recovery
|
// 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 { useSearchParams } from 'react-router-dom';
|
||||||
import { useIntl } from 'react-intl';
|
import { LayoutContainer, CliViewerToolbar } from '@/components/cli-viewer';
|
||||||
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 {
|
import {
|
||||||
useViewerStore,
|
useViewerStore,
|
||||||
useViewerLayout,
|
useViewerLayout,
|
||||||
@@ -43,14 +23,6 @@ import { useActiveCliExecutions, useInvalidateActiveCliExecutions } from '@/hook
|
|||||||
// Types
|
// 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)
|
// CLI WebSocket message types (matching CliStreamMonitorLegacy)
|
||||||
interface CliStreamStartedPayload {
|
interface CliStreamStartedPayload {
|
||||||
executionId: string;
|
executionId: string;
|
||||||
@@ -86,14 +58,7 @@ interface CliStreamErrorPayload {
|
|||||||
// Constants
|
// Constants
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
const LAYOUT_OPTIONS: LayoutOption[] = [
|
const DEFAULT_LAYOUT = 'split-h' as const;
|
||||||
{ 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';
|
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
@@ -111,41 +76,6 @@ function formatDuration(ms: number): string {
|
|||||||
return `${hours}h ${remainingMinutes}m`;
|
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
|
* Count total panes in layout
|
||||||
*/
|
*/
|
||||||
@@ -169,14 +99,16 @@ function countPanes(layout: AllotmentLayout): number {
|
|||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
export function CliViewerPage() {
|
export function CliViewerPage() {
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
// Fullscreen state
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
// Store hooks
|
// Store hooks
|
||||||
const layout = useViewerLayout();
|
const layout = useViewerLayout();
|
||||||
const panes = useViewerPanes();
|
const panes = useViewerPanes();
|
||||||
const focusedPaneId = useFocusedPaneId();
|
const focusedPaneId = useFocusedPaneId();
|
||||||
const { initializeDefaultLayout, addTab, reset } = useViewerStore();
|
const { initializeDefaultLayout, addTab } = useViewerStore();
|
||||||
|
|
||||||
// CLI Stream Store hooks
|
// CLI Stream Store hooks
|
||||||
const executions = useCliStreamStore((state) => state.executions);
|
const executions = useCliStreamStore((state) => state.executions);
|
||||||
@@ -188,24 +120,9 @@ export function CliViewerPage() {
|
|||||||
const lastMessage = useNotificationStore(selectWsLastMessage);
|
const lastMessage = useNotificationStore(selectWsLastMessage);
|
||||||
|
|
||||||
// Active execution sync from server
|
// Active execution sync from server
|
||||||
const { isLoading: _isSyncing } = useActiveCliExecutions(true); // Always sync when page is open
|
useActiveCliExecutions(true);
|
||||||
const invalidateActive = useInvalidateActiveCliExecutions();
|
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)
|
// Handle WebSocket messages for CLI stream (same logic as CliStreamMonitorLegacy)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastMessage || lastMessage === lastProcessedMsgRef.current) return;
|
if (!lastMessage || lastMessage === lastProcessedMsgRef.current) return;
|
||||||
@@ -297,30 +214,22 @@ export function CliViewerPage() {
|
|||||||
}, [lastMessage, invalidateActive]);
|
}, [lastMessage, invalidateActive]);
|
||||||
|
|
||||||
// Auto-add new executions as tabs, distributing across available panes
|
// 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());
|
const addedExecutionsRef = useRef<Set<string>>(new Set());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Get all pane IDs from the current layout
|
|
||||||
const paneIds = Object.keys(panes);
|
const paneIds = Object.keys(panes);
|
||||||
if (paneIds.length === 0) return;
|
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;
|
const storeAddTab = useViewerStore.getState().addTab;
|
||||||
|
|
||||||
// Get new executions that haven't been added yet
|
|
||||||
const newExecutionIds = Object.keys(executions).filter(
|
const newExecutionIds = Object.keys(executions).filter(
|
||||||
(id) => !addedExecutionsRef.current.has(id)
|
(id) => !addedExecutionsRef.current.has(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newExecutionIds.length === 0) return;
|
if (newExecutionIds.length === 0) return;
|
||||||
|
|
||||||
// Distribute new executions across panes round-robin
|
|
||||||
newExecutionIds.forEach((executionId, index) => {
|
newExecutionIds.forEach((executionId, index) => {
|
||||||
addedExecutionsRef.current.add(executionId);
|
addedExecutionsRef.current.add(executionId);
|
||||||
const exec = executions[executionId];
|
const exec = executions[executionId];
|
||||||
const toolShort = exec.tool.split('-')[0];
|
const toolShort = exec.tool.split('-')[0];
|
||||||
// Round-robin pane selection
|
|
||||||
const targetPaneId = paneIds[index % paneIds.length];
|
const targetPaneId = paneIds[index % paneIds.length];
|
||||||
storeAddTab(targetPaneId, executionId, `${toolShort} (${exec.mode})`);
|
storeAddTab(targetPaneId, executionId, `${toolShort} (${exec.mode})`);
|
||||||
});
|
});
|
||||||
@@ -338,10 +247,7 @@ export function CliViewerPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const executionId = searchParams.get('executionId');
|
const executionId = searchParams.get('executionId');
|
||||||
if (executionId && focusedPaneId) {
|
if (executionId && focusedPaneId) {
|
||||||
// Add tab to focused pane
|
|
||||||
addTab(focusedPaneId, executionId, `Execution ${executionId.slice(0, 8)}`);
|
addTab(focusedPaneId, executionId, `Execution ${executionId.slice(0, 8)}`);
|
||||||
|
|
||||||
// Clear the URL param after processing
|
|
||||||
setSearchParams((prev) => {
|
setSearchParams((prev) => {
|
||||||
const newParams = new URLSearchParams(prev);
|
const newParams = new URLSearchParams(prev);
|
||||||
newParams.delete('executionId');
|
newParams.delete('executionId');
|
||||||
@@ -350,104 +256,20 @@ export function CliViewerPage() {
|
|||||||
}
|
}
|
||||||
}, [searchParams, focusedPaneId, addTab, setSearchParams]);
|
}, [searchParams, focusedPaneId, addTab, setSearchParams]);
|
||||||
|
|
||||||
// Handle layout change
|
// Toggle fullscreen handler
|
||||||
const handleLayoutChange = useCallback(
|
const handleToggleFullscreen = () => {
|
||||||
(layoutType: LayoutType) => {
|
setIsFullscreen((prev) => !prev);
|
||||||
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;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
{/* ======================================== */}
|
{/* ======================================== */}
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
{/* ======================================== */}
|
{/* ======================================== */}
|
||||||
<div className="flex items-center justify-between gap-3 p-3 bg-card border-b border-border">
|
<CliViewerToolbar
|
||||||
{/* Page Title */}
|
isFullscreen={isFullscreen}
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
onToggleFullscreen={handleToggleFullscreen}
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* ======================================== */}
|
{/* ======================================== */}
|
||||||
{/* Layout Container */}
|
{/* Layout Container */}
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ interface CliToolCardProps {
|
|||||||
onUpdateAvailableModels: (models: string[]) => void;
|
onUpdateAvailableModels: (models: string[]) => void;
|
||||||
onUpdateEnvFile: (envFile: string | undefined) => void;
|
onUpdateEnvFile: (envFile: string | undefined) => void;
|
||||||
onUpdateSettingsFile: (settingsFile: string | undefined) => void;
|
onUpdateSettingsFile: (settingsFile: string | undefined) => void;
|
||||||
|
onUpdateEffort: (effort: string | undefined) => void;
|
||||||
onSaveToBackend: () => void;
|
onSaveToBackend: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +146,7 @@ function CliToolCard({
|
|||||||
onUpdateAvailableModels,
|
onUpdateAvailableModels,
|
||||||
onUpdateEnvFile,
|
onUpdateEnvFile,
|
||||||
onUpdateSettingsFile,
|
onUpdateSettingsFile,
|
||||||
|
onUpdateEffort,
|
||||||
onSaveToBackend,
|
onSaveToBackend,
|
||||||
}: CliToolCardProps) {
|
}: CliToolCardProps) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@@ -449,6 +451,39 @@ function CliToolCard({
|
|||||||
</div>
|
</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 */}
|
{/* Action Buttons */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!isDefault && config.enabled && (
|
{!isDefault && config.enabled && (
|
||||||
@@ -948,6 +983,7 @@ interface CliToolsWithStatusProps {
|
|||||||
onUpdateAvailableModels: (toolId: string, models: string[]) => void;
|
onUpdateAvailableModels: (toolId: string, models: string[]) => void;
|
||||||
onUpdateEnvFile: (toolId: string, envFile: string | undefined) => void;
|
onUpdateEnvFile: (toolId: string, envFile: string | undefined) => void;
|
||||||
onUpdateSettingsFile: (toolId: string, settingsFile: string | undefined) => void;
|
onUpdateSettingsFile: (toolId: string, settingsFile: string | undefined) => void;
|
||||||
|
onUpdateEffort: (toolId: string, effort: string | undefined) => void;
|
||||||
onSaveToBackend: (toolId: string) => void;
|
onSaveToBackend: (toolId: string) => void;
|
||||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'];
|
formatMessage: ReturnType<typeof useIntl>['formatMessage'];
|
||||||
}
|
}
|
||||||
@@ -965,6 +1001,7 @@ function CliToolsWithStatus({
|
|||||||
onUpdateAvailableModels,
|
onUpdateAvailableModels,
|
||||||
onUpdateEnvFile,
|
onUpdateEnvFile,
|
||||||
onUpdateSettingsFile,
|
onUpdateSettingsFile,
|
||||||
|
onUpdateEffort,
|
||||||
onSaveToBackend,
|
onSaveToBackend,
|
||||||
formatMessage,
|
formatMessage,
|
||||||
}: CliToolsWithStatusProps) {
|
}: CliToolsWithStatusProps) {
|
||||||
@@ -995,6 +1032,7 @@ function CliToolsWithStatus({
|
|||||||
onUpdateAvailableModels={(models) => onUpdateAvailableModels(toolId, models)}
|
onUpdateAvailableModels={(models) => onUpdateAvailableModels(toolId, models)}
|
||||||
onUpdateEnvFile={(envFile) => onUpdateEnvFile(toolId, envFile)}
|
onUpdateEnvFile={(envFile) => onUpdateEnvFile(toolId, envFile)}
|
||||||
onUpdateSettingsFile={(settingsFile) => onUpdateSettingsFile(toolId, settingsFile)}
|
onUpdateSettingsFile={(settingsFile) => onUpdateSettingsFile(toolId, settingsFile)}
|
||||||
|
onUpdateEffort={(effort) => onUpdateEffort(toolId, effort)}
|
||||||
onSaveToBackend={() => onSaveToBackend(toolId)}
|
onSaveToBackend={() => onSaveToBackend(toolId)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -1057,6 +1095,10 @@ export function SettingsPage() {
|
|||||||
updateCliTool(toolId, { settingsFile });
|
updateCliTool(toolId, { settingsFile });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateEffort = (toolId: string, effort: string | undefined) => {
|
||||||
|
updateCliTool(toolId, { effort });
|
||||||
|
};
|
||||||
|
|
||||||
// Save tool config to backend (~/.claude/cli-tools.json)
|
// Save tool config to backend (~/.claude/cli-tools.json)
|
||||||
const handleSaveToBackend = useCallback(async (toolId: string) => {
|
const handleSaveToBackend = useCallback(async (toolId: string) => {
|
||||||
const config = cliTools[toolId];
|
const config = cliTools[toolId];
|
||||||
@@ -1078,6 +1120,7 @@ export function SettingsPage() {
|
|||||||
body.envFile = config.envFile || null;
|
body.envFile = config.envFile || null;
|
||||||
} else if (configFileType === 'settingsFile') {
|
} else if (configFileType === 'settingsFile') {
|
||||||
body.settingsFile = config.settingsFile || null;
|
body.settingsFile = config.settingsFile || null;
|
||||||
|
body.effort = config.effort || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`/api/cli/config/${toolId}`, {
|
const res = await fetch(`/api/cli/config/${toolId}`, {
|
||||||
@@ -1210,6 +1253,7 @@ export function SettingsPage() {
|
|||||||
onUpdateAvailableModels={handleUpdateAvailableModels}
|
onUpdateAvailableModels={handleUpdateAvailableModels}
|
||||||
onUpdateEnvFile={handleUpdateEnvFile}
|
onUpdateEnvFile={handleUpdateEnvFile}
|
||||||
onUpdateSettingsFile={handleUpdateSettingsFile}
|
onUpdateSettingsFile={handleUpdateSettingsFile}
|
||||||
|
onUpdateEffort={handleUpdateEffort}
|
||||||
onSaveToBackend={handleSaveToBackend}
|
onSaveToBackend={handleSaveToBackend}
|
||||||
formatMessage={formatMessage}
|
formatMessage={formatMessage}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,9 +5,8 @@
|
|||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
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 { useFlowStore } from '@/stores';
|
||||||
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
|
||||||
import { useExecutionStore } from '@/stores/executionStore';
|
import { useExecutionStore } from '@/stores/executionStore';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { FlowCanvas } from './FlowCanvas';
|
import { FlowCanvas } from './FlowCanvas';
|
||||||
|
|||||||
@@ -396,6 +396,8 @@ export interface CliToolConfig {
|
|||||||
/** Path to Claude CLI settings.json, passed via --settings (claude only) */
|
/** Path to Claude CLI settings.json, passed via --settings (claude only) */
|
||||||
settingsFile?: string;
|
settingsFile?: string;
|
||||||
availableModels?: string[];
|
availableModels?: string[];
|
||||||
|
/** Default effort level for claude (low, medium, high) */
|
||||||
|
effort?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiEndpoints {
|
export interface ApiEndpoints {
|
||||||
|
|||||||
@@ -192,6 +192,8 @@ export function run(argv: string[]): void {
|
|||||||
.option('--inject-mode <mode>', 'Inject mode: none, full, progressive (default: codex=full, others=none)')
|
.option('--inject-mode <mode>', 'Inject mode: none, full, progressive (default: codex=full, others=none)')
|
||||||
// Template/Rules options
|
// Template/Rules options
|
||||||
.option('--rule <template>', 'Template name for auto-discovery (defines $PROTO and $TMPL env vars)')
|
.option('--rule <template>', 'Template name for auto-discovery (defines $PROTO and $TMPL env vars)')
|
||||||
|
// Claude-specific options
|
||||||
|
.option('--effort <level>', 'Effort level for claude session (low, medium, high)')
|
||||||
// Codex review options
|
// Codex review options
|
||||||
.option('--uncommitted', 'Review uncommitted changes (codex review)')
|
.option('--uncommitted', 'Review uncommitted changes (codex review)')
|
||||||
.option('--base <branch>', 'Review changes against base branch (codex review)')
|
.option('--base <branch>', 'Review changes against base branch (codex review)')
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ interface CliExecOptions {
|
|||||||
title?: string; // Optional title for review summary
|
title?: string; // Optional title for review summary
|
||||||
// Template/Rules options
|
// Template/Rules options
|
||||||
rule?: string; // Template name for auto-discovery (defines $PROTO and $TMPL env vars)
|
rule?: string; // Template name for auto-discovery (defines $PROTO and $TMPL env vars)
|
||||||
|
// Claude-specific options
|
||||||
|
effort?: string; // Effort level for claude: low, medium, high
|
||||||
// Output options
|
// Output options
|
||||||
raw?: boolean; // Raw output only (best for piping)
|
raw?: boolean; // Raw output only (best for piping)
|
||||||
final?: boolean; // Final agent result only (best for piping)
|
final?: boolean; // Final agent result only (best for piping)
|
||||||
@@ -612,6 +614,7 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
commit,
|
commit,
|
||||||
title,
|
title,
|
||||||
rule,
|
rule,
|
||||||
|
effort,
|
||||||
toFile,
|
toFile,
|
||||||
raw,
|
raw,
|
||||||
final: finalOnly,
|
final: finalOnly,
|
||||||
@@ -1044,7 +1047,8 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
uncommitted,
|
uncommitted,
|
||||||
base,
|
base,
|
||||||
commit,
|
commit,
|
||||||
title
|
title,
|
||||||
|
effort
|
||||||
// Rules are now concatenated directly into prompt (no env vars)
|
// Rules are now concatenated directly into prompt (no env vars)
|
||||||
}, onOutput); // Always pass onOutput for real-time dashboard streaming
|
}, onOutput); // Always pass onOutput for real-time dashboard streaming
|
||||||
|
|
||||||
@@ -1497,6 +1501,7 @@ export async function cliCommand(
|
|||||||
console.log(chalk.gray(' --mode <mode> Mode: analysis, write, auto, review (default: analysis)'));
|
console.log(chalk.gray(' --mode <mode> Mode: analysis, write, auto, review (default: analysis)'));
|
||||||
console.log(chalk.gray(' -d, --debug Enable debug logging for troubleshooting'));
|
console.log(chalk.gray(' -d, --debug Enable debug logging for troubleshooting'));
|
||||||
console.log(chalk.gray(' --model <model> Model override (supports PRIMARY_MODEL, SECONDARY_MODEL aliases)'));
|
console.log(chalk.gray(' --model <model> Model override (supports PRIMARY_MODEL, SECONDARY_MODEL aliases)'));
|
||||||
|
console.log(chalk.gray(' --effort <level> Effort level for claude (low, medium, high)'));
|
||||||
console.log(chalk.gray(' --cd <path> Working directory'));
|
console.log(chalk.gray(' --cd <path> Working directory'));
|
||||||
console.log(chalk.gray(' --includeDirs <dirs> Additional directories'));
|
console.log(chalk.gray(' --includeDirs <dirs> Additional directories'));
|
||||||
// --timeout removed - controlled by external caller (bash timeout)
|
// --timeout removed - controlled by external caller (bash timeout)
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
if (req.method === 'PUT') {
|
if (req.method === 'PUT') {
|
||||||
handlePostRequest(req, res, async (body: unknown) => {
|
handlePostRequest(req, res, async (body: unknown) => {
|
||||||
try {
|
try {
|
||||||
const updates = body as { enabled?: boolean; primaryModel?: string; secondaryModel?: string; availableModels?: string[]; tags?: string[]; envFile?: string | null; settingsFile?: string | null };
|
const updates = body as { enabled?: boolean; primaryModel?: string; secondaryModel?: string; availableModels?: string[]; tags?: string[]; envFile?: string | null; settingsFile?: string | null; effort?: string | null };
|
||||||
const updated = updateToolConfig(initialPath, tool, updates);
|
const updated = updateToolConfig(initialPath, tool, updates);
|
||||||
|
|
||||||
// Broadcast config updated event
|
// Broadcast config updated event
|
||||||
|
|||||||
@@ -186,12 +186,9 @@ async function buildFileTree(
|
|||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const isDirectory = entry.isDirectory();
|
const isDirectory = entry.isDirectory();
|
||||||
|
|
||||||
// Check if should be ignored
|
// Check if should be ignored (pass includeHidden as showAll to skip all filtering)
|
||||||
if (shouldIgnore(entry.name, gitignorePatterns, isDirectory)) {
|
if (shouldIgnore(entry.name, gitignorePatterns, isDirectory, includeHidden)) {
|
||||||
// Allow hidden files if includeHidden is true and it's .claude or .workflow
|
continue;
|
||||||
if (!includeHidden || (!entry.name.startsWith('.claude') && !entry.name.startsWith('.workflow'))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const entryPath = join(normalizedPath, entry.name);
|
const entryPath = join(normalizedPath, entry.name);
|
||||||
@@ -326,17 +323,21 @@ function parseGitignore(gitignorePath: string): string[] {
|
|||||||
* @param {string} name - File or directory name
|
* @param {string} name - File or directory name
|
||||||
* @param {string[]} patterns - Gitignore patterns
|
* @param {string[]} patterns - Gitignore patterns
|
||||||
* @param {boolean} isDirectory - Whether the entry is a directory
|
* @param {boolean} isDirectory - Whether the entry is a directory
|
||||||
|
* @param {boolean} showAll - When true, skip hardcoded excludes and hidden file filtering (only apply gitignore)
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function shouldIgnore(name: string, patterns: string[], isDirectory: boolean): boolean {
|
function shouldIgnore(name: string, patterns: string[], isDirectory: boolean, showAll: boolean = false): boolean {
|
||||||
// Always exclude certain directories
|
// When showAll is true, only apply gitignore patterns (skip hardcoded excludes and hidden files)
|
||||||
if (isDirectory && EXPLORER_EXCLUDE_DIRS.includes(name)) {
|
if (!showAll) {
|
||||||
return true;
|
// Always exclude certain directories
|
||||||
}
|
if (isDirectory && EXPLORER_EXCLUDE_DIRS.includes(name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip hidden files/directories (starting with .)
|
// Skip hidden files/directories (starting with .)
|
||||||
if (name.startsWith('.') && name !== '.claude' && name !== '.workflow') {
|
if (name.startsWith('.')) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
@@ -626,6 +627,8 @@ export async function handleFilesRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
const maxDepth = parseInt(url.searchParams.get('maxDepth') || '6', 10);
|
const maxDepth = parseInt(url.searchParams.get('maxDepth') || '6', 10);
|
||||||
const includeHidden = url.searchParams.get('includeHidden') === 'true';
|
const includeHidden = url.searchParams.get('includeHidden') === 'true';
|
||||||
|
|
||||||
|
console.log(`[Explorer] Tree request - rootPath: ${rootPath}, includeHidden: ${includeHidden}`);
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { remoteNotificationService } from '../core/services/remote-notification-
|
|||||||
import {
|
import {
|
||||||
addPendingQuestion,
|
addPendingQuestion,
|
||||||
getPendingQuestion,
|
getPendingQuestion,
|
||||||
|
updatePendingQuestion,
|
||||||
removePendingQuestion,
|
removePendingQuestion,
|
||||||
getAllPendingQuestions,
|
getAllPendingQuestions,
|
||||||
clearAllPendingQuestions,
|
clearAllPendingQuestions,
|
||||||
@@ -451,19 +452,30 @@ export async function execute(params: AskQuestionParams): Promise<ToolResult<Ask
|
|||||||
// Generate surface ID
|
// Generate surface ID
|
||||||
const surfaceId = params.surfaceId || `question-${question.id}-${Date.now()}`;
|
const surfaceId = params.surfaceId || `question-${question.id}-${Date.now()}`;
|
||||||
|
|
||||||
|
// Check if this question was restored from disk (e.g., after MCP restart)
|
||||||
|
const existingPending = getPendingQuestion(question.id);
|
||||||
|
|
||||||
// Create promise for answer
|
// Create promise for answer
|
||||||
const resultPromise = new Promise<AskQuestionResult>((resolve, reject) => {
|
const resultPromise = new Promise<AskQuestionResult>((resolve, reject) => {
|
||||||
// Store pending question
|
// Store pending question with real resolve/reject
|
||||||
const pendingQuestion: PendingQuestion = {
|
const pendingQuestion: PendingQuestion = {
|
||||||
id: question.id,
|
id: question.id,
|
||||||
surfaceId,
|
surfaceId,
|
||||||
question,
|
question,
|
||||||
timestamp: Date.now(),
|
timestamp: existingPending?.timestamp || Date.now(),
|
||||||
timeout: params.timeout || DEFAULT_TIMEOUT_MS,
|
timeout: params.timeout || DEFAULT_TIMEOUT_MS,
|
||||||
resolve,
|
resolve,
|
||||||
reject,
|
reject,
|
||||||
};
|
};
|
||||||
addPendingQuestion(pendingQuestion);
|
|
||||||
|
// If question exists (restored from disk), update it with real resolve/reject
|
||||||
|
// This fixes the "no promise attached" issue when MCP restarts
|
||||||
|
if (existingPending) {
|
||||||
|
updatePendingQuestion(question.id, pendingQuestion);
|
||||||
|
console.log(`[AskQuestion] Updated restored question "${question.id}" with real resolve/reject`);
|
||||||
|
} else {
|
||||||
|
addPendingQuestion(pendingQuestion);
|
||||||
|
}
|
||||||
|
|
||||||
// Set timeout
|
// Set timeout
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ export interface ClaudeCliTool {
|
|||||||
* Supports ~, absolute, relative, and Windows paths
|
* Supports ~, absolute, relative, and Windows paths
|
||||||
*/
|
*/
|
||||||
settingsFile?: string;
|
settingsFile?: string;
|
||||||
|
/**
|
||||||
|
* Default effort level for Claude CLI (builtin claude only)
|
||||||
|
* Passed to Claude CLI via --effort parameter
|
||||||
|
* Valid values: 'low', 'medium', 'high'
|
||||||
|
*/
|
||||||
|
effort?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CliToolName = 'gemini' | 'qwen' | 'codex' | 'claude' | 'opencode' | string;
|
export type CliToolName = 'gemini' | 'qwen' | 'codex' | 'claude' | 'opencode' | string;
|
||||||
@@ -1030,6 +1036,7 @@ export function getToolConfig(projectDir: string, tool: string): {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
envFile?: string;
|
envFile?: string;
|
||||||
settingsFile?: string;
|
settingsFile?: string;
|
||||||
|
effort?: string;
|
||||||
} {
|
} {
|
||||||
const config = loadClaudeCliTools(projectDir);
|
const config = loadClaudeCliTools(projectDir);
|
||||||
const toolConfig = config.tools[tool];
|
const toolConfig = config.tools[tool];
|
||||||
@@ -1050,7 +1057,8 @@ export function getToolConfig(projectDir: string, tool: string): {
|
|||||||
secondaryModel: toolConfig.secondaryModel ?? '',
|
secondaryModel: toolConfig.secondaryModel ?? '',
|
||||||
tags: toolConfig.tags,
|
tags: toolConfig.tags,
|
||||||
envFile: toolConfig.envFile,
|
envFile: toolConfig.envFile,
|
||||||
settingsFile: toolConfig.settingsFile
|
settingsFile: toolConfig.settingsFile,
|
||||||
|
effort: toolConfig.effort
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1068,6 +1076,7 @@ export function updateToolConfig(
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
envFile: string | null;
|
envFile: string | null;
|
||||||
settingsFile: string | null;
|
settingsFile: string | null;
|
||||||
|
effort: string | null;
|
||||||
}>
|
}>
|
||||||
): ClaudeCliToolsConfig {
|
): ClaudeCliToolsConfig {
|
||||||
const config = loadClaudeCliTools(projectDir);
|
const config = loadClaudeCliTools(projectDir);
|
||||||
@@ -1104,6 +1113,14 @@ export function updateToolConfig(
|
|||||||
config.tools[tool].settingsFile = updates.settingsFile;
|
config.tools[tool].settingsFile = updates.settingsFile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Handle effort: set to undefined if null/empty, otherwise set value
|
||||||
|
if (updates.effort !== undefined) {
|
||||||
|
if (updates.effort === null || updates.effort === '') {
|
||||||
|
delete config.tools[tool].effort;
|
||||||
|
} else {
|
||||||
|
config.tools[tool].effort = updates.effort;
|
||||||
|
}
|
||||||
|
}
|
||||||
saveClaudeCliTools(projectDir, config);
|
saveClaudeCliTools(projectDir, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface CliToolConfig {
|
|||||||
envFile?: string | null;
|
envFile?: string | null;
|
||||||
type?: 'builtin' | 'cli-wrapper' | 'api-endpoint'; // Tool type for frontend routing
|
type?: 'builtin' | 'cli-wrapper' | 'api-endpoint'; // Tool type for frontend routing
|
||||||
settingsFile?: string | null; // Claude CLI settings file path
|
settingsFile?: string | null; // Claude CLI settings file path
|
||||||
|
effort?: string | null; // Effort level for Claude CLI (low, medium, high)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CliConfig {
|
export interface CliConfig {
|
||||||
@@ -160,7 +161,8 @@ export function getFullConfigResponse(baseDir: string): {
|
|||||||
tags: tool.tags,
|
tags: tool.tags,
|
||||||
envFile: tool.envFile,
|
envFile: tool.envFile,
|
||||||
type: tool.type, // Preserve type field for frontend routing
|
type: tool.type, // Preserve type field for frontend routing
|
||||||
settingsFile: tool.settingsFile // Preserve settingsFile for Claude CLI
|
settingsFile: tool.settingsFile, // Preserve settingsFile for Claude CLI
|
||||||
|
effort: tool.effort // Preserve effort level for Claude CLI
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -429,6 +429,8 @@ const ParamsSchema = z.object({
|
|||||||
base: z.string().optional(), // Review changes against base branch
|
base: z.string().optional(), // Review changes against base branch
|
||||||
commit: z.string().optional(), // Review changes from specific commit
|
commit: z.string().optional(), // Review changes from specific commit
|
||||||
title: z.string().optional(), // Optional title for review summary
|
title: z.string().optional(), // Optional title for review summary
|
||||||
|
// Claude-specific options
|
||||||
|
effort: z.enum(['low', 'medium', 'high']).optional(), // Effort level for claude
|
||||||
// Rules env vars (PROTO, TMPL) - will be passed to subprocess environment
|
// Rules env vars (PROTO, TMPL) - will be passed to subprocess environment
|
||||||
rulesEnv: z.object({
|
rulesEnv: z.object({
|
||||||
PROTO: z.string().optional(),
|
PROTO: z.string().optional(),
|
||||||
@@ -458,7 +460,7 @@ async function executeCliTool(
|
|||||||
throw new Error(`Invalid params: ${parsed.error.message}`);
|
throw new Error(`Invalid params: ${parsed.error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tool, prompt, mode, format, model, cd, includeDirs, resume, id: customId, noNative, category, parentExecutionId, outputFormat, uncommitted, base, commit, title, rulesEnv } = parsed.data;
|
const { tool, prompt, mode, format, model, cd, includeDirs, resume, id: customId, noNative, category, parentExecutionId, outputFormat, uncommitted, base, commit, title, effort, rulesEnv } = parsed.data;
|
||||||
|
|
||||||
// Validate and determine working directory early (needed for conversation lookup)
|
// Validate and determine working directory early (needed for conversation lookup)
|
||||||
let workingDir: string;
|
let workingDir: string;
|
||||||
@@ -881,6 +883,7 @@ async function executeCliTool(
|
|||||||
|
|
||||||
// Load and validate settings file for Claude tool (builtin only)
|
// Load and validate settings file for Claude tool (builtin only)
|
||||||
let settingsFilePath: string | undefined;
|
let settingsFilePath: string | undefined;
|
||||||
|
let effectiveEffort = effort;
|
||||||
if (tool === 'claude') {
|
if (tool === 'claude') {
|
||||||
const toolConfig = getToolConfig(workingDir, tool);
|
const toolConfig = getToolConfig(workingDir, tool);
|
||||||
if (toolConfig.settingsFile) {
|
if (toolConfig.settingsFile) {
|
||||||
@@ -896,6 +899,11 @@ async function executeCliTool(
|
|||||||
errorLog('SETTINGS_FILE', `Failed to resolve Claude settings file`, { configured: toolConfig.settingsFile, error: (err as Error).message });
|
errorLog('SETTINGS_FILE', `Failed to resolve Claude settings file`, { configured: toolConfig.settingsFile, error: (err as Error).message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Use default effort from config if not explicitly provided, fallback to 'high'
|
||||||
|
if (!effectiveEffort) {
|
||||||
|
effectiveEffort = toolConfig.effort || 'high';
|
||||||
|
debugLog('EFFORT', `Using effort level`, { effort: effectiveEffort, source: toolConfig.effort ? 'config' : 'default' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build command
|
// Build command
|
||||||
@@ -908,7 +916,8 @@ async function executeCliTool(
|
|||||||
include: includeDirs,
|
include: includeDirs,
|
||||||
nativeResume: nativeResumeConfig,
|
nativeResume: nativeResumeConfig,
|
||||||
settingsFile: settingsFilePath,
|
settingsFile: settingsFilePath,
|
||||||
reviewOptions: mode === 'review' ? { uncommitted, base, commit, title } : undefined
|
reviewOptions: mode === 'review' ? { uncommitted, base, commit, title } : undefined,
|
||||||
|
effort: effectiveEffort
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use auto-detected format (from buildCommand) if available, otherwise use passed outputFormat
|
// Use auto-detected format (from buildCommand) if available, otherwise use passed outputFormat
|
||||||
|
|||||||
@@ -166,8 +166,10 @@ export function buildCommand(params: {
|
|||||||
commit?: string;
|
commit?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
};
|
};
|
||||||
|
/** Effort level for claude (low, medium, high) */
|
||||||
|
effort?: string;
|
||||||
}): { command: string; args: string[]; useStdin: boolean; outputFormat?: 'text' | 'json-lines' } {
|
}): { command: string; args: string[]; useStdin: boolean; outputFormat?: 'text' | 'json-lines' } {
|
||||||
const { tool, prompt, mode = 'analysis', model, dir, include, nativeResume, settingsFile, reviewOptions } = params;
|
const { tool, prompt, mode = 'analysis', model, dir, include, nativeResume, settingsFile, reviewOptions, effort } = params;
|
||||||
|
|
||||||
debugLog('BUILD_CMD', `Building command for tool: ${tool}`, {
|
debugLog('BUILD_CMD', `Building command for tool: ${tool}`, {
|
||||||
mode,
|
mode,
|
||||||
@@ -331,6 +333,10 @@ export function buildCommand(params: {
|
|||||||
if (model) {
|
if (model) {
|
||||||
args.push('--model', model);
|
args.push('--model', model);
|
||||||
}
|
}
|
||||||
|
// Effort level: claude --effort <low|medium|high>
|
||||||
|
if (effort) {
|
||||||
|
args.push('--effort', effort);
|
||||||
|
}
|
||||||
// Permission modes: write/auto → bypassPermissions, analysis → default
|
// Permission modes: write/auto → bypassPermissions, analysis → default
|
||||||
if (mode === 'write' || mode === 'auto') {
|
if (mode === 'write' || mode === 'auto') {
|
||||||
args.push('--permission-mode', 'bypassPermissions');
|
args.push('--permission-mode', 'bypassPermissions');
|
||||||
|
|||||||
Reference in New Issue
Block a user