From ddbe12b7afd011476230dbc1f958f11d6d3f5472 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Thu, 12 Feb 2026 23:53:11 +0800 Subject: [PATCH] feat: add terminal panel components and Zustand store for state management - Created a barrel export file for terminal panel components. - Implemented Zustand store for managing terminal panel UI state, including visibility, active terminal, view mode, and terminal ordering. - Added actions for opening/closing the terminal panel, setting the active terminal, changing view modes, and managing terminal order. - Introduced selectors for accessing terminal panel state properties. --- .../api-settings/CliSettingsModal.tsx | 8 - .../issue/queue/ExecutionGroup.test.tsx | 2 +- .../components/issue/queue/QueueCard.test.tsx | 9 +- .../src/components/layout/AppShell.tsx | 4 - .../src/components/layout/Header.test.tsx | 2 +- .../src/components/mcp/ConfigTypeToggle.tsx | 2 - .../src/components/mcp/CrossCliSyncPanel.tsx | 2 - .../components/mcp/RecommendedMcpSection.tsx | 1 - .../notification/NotificationPanel.tsx | 12 +- .../components/orchestrator/ToolCallCard.tsx | 1 + .../orchestrator/ToolCallsTimeline.tsx | 2 +- .../CliStreamMonitor/components/JsonCard.tsx | 5 - .../CliStreamMonitor/components/JsonField.tsx | 2 +- .../components/OutputLine.tsx | 1 - .../messages/AssistantMessage.tsx | 1 - .../messages/ErrorMessage.tsx | 1 - .../CliStreamMonitor/messages/UserMessage.tsx | 1 - .../shared/CliStreamMonitorLegacy.tsx | 8 +- .../components/shared/InsightDetailPanel.tsx | 6 +- .../src/components/shared/InsightsPanel.tsx | 1 - .../components/shared/LogBlock/LogBlock.tsx | 2 +- .../shared/LogBlock/LogBlockList.tsx | 2 +- .../src/components/shared/PromptStats.tsx | 1 - .../src/components/shared/TickerMarquee.tsx | 2 - .../src/components/shared/VersionCheck.tsx | 2 +- .../components/shared/VersionCheckModal.tsx | 2 +- .../src/components/team/TeamHeader.tsx | 2 +- .../src/components/team/TeamMessageFeed.tsx | 3 +- .../src/components/team/TeamPipeline.tsx | 12 - .../terminal-panel/TerminalMainArea.tsx | 323 ++++++++++++++++++ .../terminal-panel/TerminalNavBar.tsx | 142 ++++++++ .../terminal-panel/TerminalPanel.tsx | 71 ++++ .../src/components/terminal-panel/index.ts | 8 + .../src/components/ui/ContextAssembler.tsx | 2 +- .../src/components/ui/MultiNodeSelector.tsx | 2 +- .../src/hooks/useActiveCliExecutions.ts | 1 - ccw/frontend/src/hooks/useApiSettings.ts | 2 - ccw/frontend/src/hooks/useGraphData.ts | 8 +- ccw/frontend/src/hooks/useIssues.test.tsx | 1 - ccw/frontend/src/hooks/useIssues.ts | 1 - ccw/frontend/src/hooks/useMcpServers.ts | 3 +- ccw/frontend/src/hooks/useSessions.ts | 2 +- ccw/frontend/src/hooks/useSkills.ts | 1 - ccw/frontend/src/hooks/useWebSocket.ts | 4 +- .../src/hooks/useWorkflowStatusCounts.ts | 3 +- ccw/frontend/src/lib/api.mcp.test.ts | 2 +- .../__tests__/A2UIComponentRegistry.test.ts | 2 +- .../a2ui-runtime/__tests__/A2UIParser.test.ts | 3 +- .../__tests__/components.test.tsx | 4 +- .../a2ui-runtime/renderer/A2UIRenderer.tsx | 21 +- .../renderer/components/A2UIButton.tsx | 13 +- .../renderer/components/A2UICLIOutput.tsx | 2 +- .../renderer/components/A2UICard.tsx | 10 +- .../renderer/components/A2UICheckbox.tsx | 11 +- .../renderer/components/A2UIDateTimeInput.tsx | 8 +- .../renderer/components/A2UIProgress.tsx | 10 +- .../renderer/components/A2UIRadioGroup.tsx | 11 +- .../renderer/components/A2UIText.tsx | 10 +- .../renderer/components/A2UITextArea.tsx | 11 +- .../renderer/components/A2UITextField.tsx | 11 +- ccw/frontend/src/pages/CliViewerPage.tsx | 2 +- .../src/pages/CodexLensManagerPage.test.tsx | 1 - .../src/pages/CodexLensManagerPage.tsx | 3 +- ccw/frontend/src/pages/DiscoveryPage.test.tsx | 2 +- .../src/pages/ExecutionMonitorPage.tsx | 3 +- ccw/frontend/src/pages/IssueHubPage.tsx | 2 +- ccw/frontend/src/pages/IssueManagerPage.tsx | 2 +- ccw/frontend/src/pages/LiteTaskDetailPage.tsx | 43 --- ccw/frontend/src/stores/index.ts | 18 + ccw/frontend/src/stores/terminalPanelStore.ts | 147 ++++++++ ccw/frontend/src/types/execution.ts | 3 + ccw/frontend/tsc-errors.txt | 281 +++++++++++++++ 72 files changed, 1055 insertions(+), 254 deletions(-) create mode 100644 ccw/frontend/src/components/terminal-panel/TerminalMainArea.tsx create mode 100644 ccw/frontend/src/components/terminal-panel/TerminalNavBar.tsx create mode 100644 ccw/frontend/src/components/terminal-panel/TerminalPanel.tsx create mode 100644 ccw/frontend/src/components/terminal-panel/index.ts create mode 100644 ccw/frontend/src/stores/terminalPanelStore.ts create mode 100644 ccw/frontend/tsc-errors.txt diff --git a/ccw/frontend/src/components/api-settings/CliSettingsModal.tsx b/ccw/frontend/src/components/api-settings/CliSettingsModal.tsx index bb1c46d2..f067ef8e 100644 --- a/ccw/frontend/src/components/api-settings/CliSettingsModal.tsx +++ b/ccw/frontend/src/components/api-settings/CliSettingsModal.tsx @@ -37,14 +37,6 @@ type ModeType = 'provider-based' | 'direct'; // ========== Helper Functions ========== -function safeStringifyConfig(config: unknown): string { - try { - return JSON.stringify(config ?? {}, null, 2); - } catch { - return '{}'; - } -} - function parseConfigJson( configJson: string ): { ok: true; value: Record } | { ok: false; errorKey: string } { diff --git a/ccw/frontend/src/components/issue/queue/ExecutionGroup.test.tsx b/ccw/frontend/src/components/issue/queue/ExecutionGroup.test.tsx index d2af114b..f186ba96 100644 --- a/ccw/frontend/src/components/issue/queue/ExecutionGroup.test.tsx +++ b/ccw/frontend/src/components/issue/queue/ExecutionGroup.test.tsx @@ -127,7 +127,7 @@ describe('ExecutionGroup', () => { render(, { locale: 'en' }); // Parallel items should not have numbers in the numbering position - const numberElements = document.querySelectorAll('.text-muted-foreground.text-xs'); + document.querySelectorAll('.text-muted-foreground.text-xs'); // In parallel mode, the numbering position should be empty }); }); diff --git a/ccw/frontend/src/components/issue/queue/QueueCard.test.tsx b/ccw/frontend/src/components/issue/queue/QueueCard.test.tsx index b59688af..b8866937 100644 --- a/ccw/frontend/src/components/issue/queue/QueueCard.test.tsx +++ b/ccw/frontend/src/components/issue/queue/QueueCard.test.tsx @@ -17,8 +17,13 @@ describe('QueueCard', () => { }; const mockQueue: IssueQueue = { - tasks: ['task1', 'task2'], - solutions: ['solution1'], + tasks: [ + { item_id: 'task1', issue_id: 'issue-1', solution_id: 'sol-1', status: 'pending', execution_order: 1, execution_group: 'group-1', depends_on: [], semantic_priority: 1 }, + { item_id: 'task2', issue_id: 'issue-1', solution_id: 'sol-1', status: 'pending', execution_order: 2, execution_group: 'group-1', depends_on: [], semantic_priority: 1 }, + ], + solutions: [ + { item_id: 'solution1', issue_id: 'issue-1', solution_id: 'sol-1', status: 'pending', execution_order: 1, execution_group: 'group-1', depends_on: [], semantic_priority: 1 }, + ], conflicts: [], execution_groups: ['group-1'], grouped_items: mockQueueItems, diff --git a/ccw/frontend/src/components/layout/AppShell.tsx b/ccw/frontend/src/components/layout/AppShell.tsx index 15170ef0..4d2f9b08 100644 --- a/ccw/frontend/src/components/layout/AppShell.tsx +++ b/ccw/frontend/src/components/layout/AppShell.tsx @@ -127,10 +127,6 @@ export function AppShell({ return () => window.removeEventListener('resize', handleResize); }, []); - const handleMenuClick = useCallback(() => { - setMobileOpen((prev) => !prev); - }, []); - const handleMobileClose = useCallback(() => { setMobileOpen(false); }, []); diff --git a/ccw/frontend/src/components/layout/Header.test.tsx b/ccw/frontend/src/components/layout/Header.test.tsx index d29bef09..30663904 100644 --- a/ccw/frontend/src/components/layout/Header.test.tsx +++ b/ccw/frontend/src/components/layout/Header.test.tsx @@ -42,7 +42,7 @@ describe('Header Component - i18n Tests', () => { describe('translated aria-labels', () => { it('should have translated aria-label for menu toggle', () => { - render(
); + render(
); const menuButton = screen.getByRole('button', { name: /toggle navigation/i }); expect(menuButton).toBeInTheDocument(); diff --git a/ccw/frontend/src/components/mcp/ConfigTypeToggle.tsx b/ccw/frontend/src/components/mcp/ConfigTypeToggle.tsx index 1f64abf5..978537c5 100644 --- a/ccw/frontend/src/components/mcp/ConfigTypeToggle.tsx +++ b/ccw/frontend/src/components/mcp/ConfigTypeToggle.tsx @@ -15,8 +15,6 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/AlertDialog'; -import { Button } from '@/components/ui/Button'; -import { Badge } from '@/components/ui/Badge'; import { cn } from '@/lib/utils'; // ========== Types ========== diff --git a/ccw/frontend/src/components/mcp/CrossCliSyncPanel.tsx b/ccw/frontend/src/components/mcp/CrossCliSyncPanel.tsx index f390520d..d61c6718 100644 --- a/ccw/frontend/src/components/mcp/CrossCliSyncPanel.tsx +++ b/ccw/frontend/src/components/mcp/CrossCliSyncPanel.tsx @@ -30,8 +30,6 @@ interface ServerCheckboxItem { selected: boolean; } -type CopyDirection = 'to-codex' | 'from-codex'; - // ========== Component ========== export function CrossCliSyncPanel({ onSuccess, className }: CrossCliSyncPanelProps) { diff --git a/ccw/frontend/src/components/mcp/RecommendedMcpSection.tsx b/ccw/frontend/src/components/mcp/RecommendedMcpSection.tsx index 0da00293..58dc87f1 100644 --- a/ccw/frontend/src/components/mcp/RecommendedMcpSection.tsx +++ b/ccw/frontend/src/components/mcp/RecommendedMcpSection.tsx @@ -10,7 +10,6 @@ import { Globe, Sparkles, Download, - Check, Settings, Key, Zap, diff --git a/ccw/frontend/src/components/notification/NotificationPanel.tsx b/ccw/frontend/src/components/notification/NotificationPanel.tsx index c7e4e053..19ee55f4 100644 --- a/ccw/frontend/src/components/notification/NotificationPanel.tsx +++ b/ccw/frontend/src/components/notification/NotificationPanel.tsx @@ -21,7 +21,6 @@ import { Loader2, RotateCcw, Code, - Image as ImageIcon, Database, Mail, MailOpen, @@ -67,15 +66,7 @@ function formatTimeAgo(timestamp: string, formatMessage: (message: { id: string; return new Date(timestamp).toLocaleDateString(); } -function formatDetails(details: unknown): string { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - if (typeof details === 'string') return details; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - if (typeof details === 'object' && details !== null) { - return JSON.stringify(details, null, 2); - } - return String(details); -} +// ========== Main Types ========== function getNotificationIcon(type: Toast['type']) { const iconClassName = 'h-4 w-4 shrink-0'; @@ -718,7 +709,6 @@ export interface NotificationPanelProps { } export function NotificationPanel({ isOpen, onClose }: NotificationPanelProps) { - const { formatMessage } = useIntl(); // Store state const persistentNotifications = useNotificationStore(selectPersistentNotifications); diff --git a/ccw/frontend/src/components/orchestrator/ToolCallCard.tsx b/ccw/frontend/src/components/orchestrator/ToolCallCard.tsx index eada10dc..f729f675 100644 --- a/ccw/frontend/src/components/orchestrator/ToolCallCard.tsx +++ b/ccw/frontend/src/components/orchestrator/ToolCallCard.tsx @@ -15,6 +15,7 @@ import { Terminal, Wrench, FileEdit, + FileText, Brain, Search, } from 'lucide-react'; diff --git a/ccw/frontend/src/components/orchestrator/ToolCallsTimeline.tsx b/ccw/frontend/src/components/orchestrator/ToolCallsTimeline.tsx index aebc586e..cb3994a8 100644 --- a/ccw/frontend/src/components/orchestrator/ToolCallsTimeline.tsx +++ b/ccw/frontend/src/components/orchestrator/ToolCallsTimeline.tsx @@ -3,7 +3,7 @@ // ======================================== // Vertical timeline displaying tool calls in chronological order -import React, { memo, useMemo, useCallback, useEffect, useRef } from 'react'; +import { memo, useMemo, useCallback, useEffect, useRef } from 'react'; import { Wrench, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { ToolCallCard } from './ToolCallCard'; diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonCard.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonCard.tsx index 739d86f1..4a46604c 100644 --- a/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonCard.tsx +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonCard.tsx @@ -10,12 +10,9 @@ import { Info, Code, Copy, - ChevronRight, AlertTriangle, Brain, } from 'lucide-react'; -import { Button } from '@/components/ui/Button'; -import { Badge } from '@/components/ui/Badge'; import { cn } from '@/lib/utils'; import { JsonField } from './JsonField'; import ReactMarkdown from 'react-markdown'; @@ -94,8 +91,6 @@ const TYPE_CONFIGS: Record = { export function JsonCard({ data, type, - timestamp, - onCopy, }: JsonCardProps) { const [isExpanded, setIsExpanded] = useState(true); diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonField.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonField.tsx index 1f985cdd..eb61160f 100644 --- a/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonField.tsx +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonField.tsx @@ -9,7 +9,7 @@ export interface JsonFieldProps { export function JsonField({ fieldName, value }: JsonFieldProps) { const [isExpanded, setIsExpanded] = useState(false); - const [copied, setCopied] = useState(false); + const [, setCopied] = useState(false); const isObject = value !== null && typeof value === 'object'; const isNested = isObject && (Array.isArray(value) || Object.keys(value).length > 0); diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/components/OutputLine.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/components/OutputLine.tsx index 821ccad7..6faa783f 100644 --- a/ccw/frontend/src/components/shared/CliStreamMonitor/components/OutputLine.tsx +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/components/OutputLine.tsx @@ -5,7 +5,6 @@ import { useMemo } from 'react'; import { Brain, Settings, AlertCircle, Info, MessageCircle, Wrench } from 'lucide-react'; -import { cn } from '@/lib/utils'; import { JsonCard } from './JsonCard'; import { detectJsonInLine } from '../utils/jsonDetector'; diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/messages/AssistantMessage.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/AssistantMessage.tsx index 5aa749d9..a3f19029 100644 --- a/ccw/frontend/src/components/shared/CliStreamMonitor/messages/AssistantMessage.tsx +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/AssistantMessage.tsx @@ -91,7 +91,6 @@ export function AssistantMessage({ onCopy, className }: AssistantMessageProps) { - const { formatMessage } = useIntl(); const [isExpanded, setIsExpanded] = useState(true); const [copied, setCopied] = useState(false); diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/messages/ErrorMessage.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/ErrorMessage.tsx index 2f8c7210..c051ad62 100644 --- a/ccw/frontend/src/components/shared/CliStreamMonitor/messages/ErrorMessage.tsx +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/ErrorMessage.tsx @@ -19,7 +19,6 @@ export interface ErrorMessageProps { export function ErrorMessage({ title, message, - timestamp, onRetry, onDismiss, className diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/messages/UserMessage.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/UserMessage.tsx index eeb8095a..e86f8700 100644 --- a/ccw/frontend/src/components/shared/CliStreamMonitor/messages/UserMessage.tsx +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/UserMessage.tsx @@ -18,7 +18,6 @@ export interface UserMessageProps { export function UserMessage({ content, - timestamp, onCopy, onViewRaw, className diff --git a/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx b/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx index c5715d01..1580473f 100644 --- a/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx +++ b/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx @@ -154,7 +154,7 @@ interface OutputLineCardProps { onCopy?: (content: string) => void; } -function OutputLineCard({ group, onCopy }: OutputLineCardProps) { +function OutputLineCard({ group }: OutputLineCardProps) { const borderColor = getBorderColorForType(group.type); // Extract content from all lines in the group @@ -357,12 +357,6 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) { }, 50); // 50ms debounce }, []); - // Scroll to bottom handler - const scrollToBottom = useCallback(() => { - logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - setIsUserScrolling(false); - }, []); - // Handle closing an execution tab const handleCloseExecution = useCallback((executionId: string) => { // Mark as closed by user so it won't be re-added by server sync diff --git a/ccw/frontend/src/components/shared/InsightDetailPanel.tsx b/ccw/frontend/src/components/shared/InsightDetailPanel.tsx index 6a0c6a1f..82a53736 100644 --- a/ccw/frontend/src/components/shared/InsightDetailPanel.tsx +++ b/ccw/frontend/src/components/shared/InsightDetailPanel.tsx @@ -3,7 +3,6 @@ // ======================================== // Display detailed view of a single insight with patterns, suggestions, and metadata -import * as React from 'react'; import { useIntl } from 'react-intl'; import { cn } from '@/lib/utils'; import { @@ -133,8 +132,7 @@ function formatRelativeTime(timestamp: string, locale: string): string { /** * PatternItem component for displaying a single pattern */ -function PatternItem({ pattern, locale }: { pattern: Pattern; locale: string }) { - const { formatMessage } = useIntl(); +function PatternItem({ pattern }: { pattern: Pattern; locale: string }) { const severity = pattern.severity ?? 'info'; const config = severityConfig[severity] ?? severityConfig.default; @@ -177,7 +175,7 @@ function PatternItem({ pattern, locale }: { pattern: Pattern; locale: string }) /** * SuggestionItem component for displaying a single suggestion */ -function SuggestionItem({ suggestion, locale }: { suggestion: Suggestion; locale: string }) { +function SuggestionItem({ suggestion }: { suggestion: Suggestion; locale: string }) { const { formatMessage } = useIntl(); const config = suggestionTypeConfig[suggestion.type] ?? suggestionTypeConfig.refactor; const typeLabel = formatMessage({ id: `prompts.suggestions.types.${suggestion.type}` }); diff --git a/ccw/frontend/src/components/shared/InsightsPanel.tsx b/ccw/frontend/src/components/shared/InsightsPanel.tsx index 06a8cbc0..be5a975a 100644 --- a/ccw/frontend/src/components/shared/InsightsPanel.tsx +++ b/ccw/frontend/src/components/shared/InsightsPanel.tsx @@ -3,7 +3,6 @@ // ======================================== // AI insights panel for prompt history analysis -import * as React from 'react'; import { useIntl } from 'react-intl'; import { cn } from '@/lib/utils'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; diff --git a/ccw/frontend/src/components/shared/LogBlock/LogBlock.tsx b/ccw/frontend/src/components/shared/LogBlock/LogBlock.tsx index 6133c365..7b59ef5f 100644 --- a/ccw/frontend/src/components/shared/LogBlock/LogBlock.tsx +++ b/ccw/frontend/src/components/shared/LogBlock/LogBlock.tsx @@ -2,7 +2,7 @@ // LogBlock Component // ======================================== -import React, { memo } from 'react'; +import { memo } from 'react'; import { ChevronDown, ChevronUp, diff --git a/ccw/frontend/src/components/shared/LogBlock/LogBlockList.tsx b/ccw/frontend/src/components/shared/LogBlock/LogBlockList.tsx index d5049b7d..1824e5e8 100644 --- a/ccw/frontend/src/components/shared/LogBlock/LogBlockList.tsx +++ b/ccw/frontend/src/components/shared/LogBlock/LogBlockList.tsx @@ -3,7 +3,7 @@ // ======================================== // Container component for displaying grouped CLI output blocks -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback } from 'react'; import { useCliStreamStore, type LogBlockData } from '@/stores/cliStreamStore'; import { LogBlock } from './LogBlock'; diff --git a/ccw/frontend/src/components/shared/PromptStats.tsx b/ccw/frontend/src/components/shared/PromptStats.tsx index 0463f093..44cc2bbf 100644 --- a/ccw/frontend/src/components/shared/PromptStats.tsx +++ b/ccw/frontend/src/components/shared/PromptStats.tsx @@ -3,7 +3,6 @@ // ======================================== // Statistics display for prompt history -import * as React from 'react'; import { useIntl } from 'react-intl'; import { StatCard, StatCardSkeleton } from '@/components/shared/StatCard'; import { MessageSquare, FileType, Hash, Star } from 'lucide-react'; diff --git a/ccw/frontend/src/components/shared/TickerMarquee.tsx b/ccw/frontend/src/components/shared/TickerMarquee.tsx index 1a8dd4cc..22d892b4 100644 --- a/ccw/frontend/src/components/shared/TickerMarquee.tsx +++ b/ccw/frontend/src/components/shared/TickerMarquee.tsx @@ -3,14 +3,12 @@ // ======================================== // Real-time scrolling ticker with CSS marquee animation and WebSocket messages -import * as React from 'react'; import { useIntl } from 'react-intl'; import { cn } from '@/lib/utils'; import { useRealtimeUpdates, type TickerMessage } from '@/hooks/useRealtimeUpdates'; import { Play, CheckCircle2, - XCircle, Workflow, Activity, WifiOff, diff --git a/ccw/frontend/src/components/shared/VersionCheck.tsx b/ccw/frontend/src/components/shared/VersionCheck.tsx index 5a781ab3..c408eec3 100644 --- a/ccw/frontend/src/components/shared/VersionCheck.tsx +++ b/ccw/frontend/src/components/shared/VersionCheck.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Badge } from '../ui/badge'; +import { Badge } from '../ui/Badge'; import { toast } from 'sonner'; import { VersionCheckModal } from './VersionCheckModal'; diff --git a/ccw/frontend/src/components/shared/VersionCheckModal.tsx b/ccw/frontend/src/components/shared/VersionCheckModal.tsx index 5b44b132..7637bcd4 100644 --- a/ccw/frontend/src/components/shared/VersionCheckModal.tsx +++ b/ccw/frontend/src/components/shared/VersionCheckModal.tsx @@ -3,7 +3,7 @@ import { DialogContent, DialogHeader, DialogTitle, -} from '../ui/dialog'; +} from '../ui/Dialog'; interface VersionCheckModalProps { currentVersion: string; diff --git a/ccw/frontend/src/components/team/TeamHeader.tsx b/ccw/frontend/src/components/team/TeamHeader.tsx index ef3c8051..395f3992 100644 --- a/ccw/frontend/src/components/team/TeamHeader.tsx +++ b/ccw/frontend/src/components/team/TeamHeader.tsx @@ -4,7 +4,7 @@ // Team selector, stats chips, and controls import { useIntl } from 'react-intl'; -import { Users, MessageSquare, Clock, RefreshCw } from 'lucide-react'; +import { Users, MessageSquare, RefreshCw } from 'lucide-react'; import { Badge } from '@/components/ui/Badge'; import { Switch } from '@/components/ui/Switch'; import { Label } from '@/components/ui/Label'; diff --git a/ccw/frontend/src/components/team/TeamMessageFeed.tsx b/ccw/frontend/src/components/team/TeamMessageFeed.tsx index 3132c25f..3592d9a5 100644 --- a/ccw/frontend/src/components/team/TeamMessageFeed.tsx +++ b/ccw/frontend/src/components/team/TeamMessageFeed.tsx @@ -7,7 +7,6 @@ import { useState, useMemo } from 'react'; import { useIntl } from 'react-intl'; import { ChevronDown, ChevronUp, FileText, Filter, X } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/Card'; -import { Badge } from '@/components/ui/Badge'; import { Button } from '@/components/ui/Button'; import { Select, @@ -17,7 +16,7 @@ import { SelectValue, } from '@/components/ui/Select'; import { cn } from '@/lib/utils'; -import type { TeamMessage, TeamMessageType, TeamMessageFilter } from '@/types/team'; +import type { TeamMessage, TeamMessageFilter } from '@/types/team'; interface TeamMessageFeedProps { messages: TeamMessage[]; diff --git a/ccw/frontend/src/components/team/TeamPipeline.tsx b/ccw/frontend/src/components/team/TeamPipeline.tsx index 5690147f..b6f6164a 100644 --- a/ccw/frontend/src/components/team/TeamPipeline.tsx +++ b/ccw/frontend/src/components/team/TeamPipeline.tsx @@ -104,18 +104,6 @@ function Arrow() { ); } -function ForkArrow() { - return ( -
-
-
-
-
-
-
- ); -} - export function TeamPipeline({ messages }: TeamPipelineProps) { const { formatMessage } = useIntl(); const stageStatus = derivePipelineStatus(messages); diff --git a/ccw/frontend/src/components/terminal-panel/TerminalMainArea.tsx b/ccw/frontend/src/components/terminal-panel/TerminalMainArea.tsx new file mode 100644 index 00000000..93eddd40 --- /dev/null +++ b/ccw/frontend/src/components/terminal-panel/TerminalMainArea.tsx @@ -0,0 +1,323 @@ +// ======================================== +// TerminalMainArea Component +// ======================================== +// Main display area for the terminal panel. +// Shows header with session info, tab switcher (terminal/queue), and +// embedded xterm.js terminal with command input. Reuses the xterm rendering +// pattern from IssueTerminalTab (init, FitAddon, output streaming, PTY input). + +import { useEffect, useRef, useState } from 'react'; +import { Terminal as XTerm } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; +import { X, Send } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs'; +import { cn } from '@/lib/utils'; +import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; +import { useTerminalPanelStore } from '@/stores/terminalPanelStore'; +import { useCliSessionStore } from '@/stores/cliSessionStore'; +import type { PanelView } from '@/stores/terminalPanelStore'; +import { + fetchCliSessionBuffer, + sendCliSessionText, + resizeCliSession, + executeInCliSession, +} from '@/lib/api'; + +// ========== Types ========== + +export interface TerminalMainAreaProps { + onClose: () => void; +} + +// ========== Component ========== + +export function TerminalMainArea({ onClose }: TerminalMainAreaProps) { + const projectPath = useWorkflowStore(selectProjectPath); + + const activeTerminalId = useTerminalPanelStore((s) => s.activeTerminalId); + const panelView = useTerminalPanelStore((s) => s.panelView); + const setPanelView = useTerminalPanelStore((s) => s.setPanelView); + + const sessionsByKey = useCliSessionStore((s) => s.sessions); + const outputChunks = useCliSessionStore((s) => s.outputChunks); + const setBuffer = useCliSessionStore((s) => s.setBuffer); + const clearOutput = useCliSessionStore((s) => s.clearOutput); + + const activeSession = activeTerminalId ? sessionsByKey[activeTerminalId] : null; + + const [prompt, setPrompt] = useState(''); + const [isExecuting, setIsExecuting] = useState(false); + const [error, setError] = useState(null); + + // xterm refs + const terminalHostRef = useRef(null); + const xtermRef = useRef(null); + const fitAddonRef = useRef(null); + const lastChunkIndexRef = useRef(0); + + // Input batching refs (same pattern as IssueTerminalTab) + const pendingInputRef = useRef(''); + const flushTimerRef = useRef(null); + const activeSessionKeyRef = useRef(null); + + // Keep ref in sync with activeTerminalId for closures + useEffect(() => { + activeSessionKeyRef.current = activeTerminalId; + }, [activeTerminalId]); + + const flushInput = async () => { + const sessionKey = activeSessionKeyRef.current; + if (!sessionKey) return; + const pending = pendingInputRef.current; + pendingInputRef.current = ''; + if (!pending) return; + try { + await sendCliSessionText(sessionKey, { text: pending, appendNewline: false }, projectPath || undefined); + } catch { + // Ignore transient failures + } + }; + + const scheduleFlush = () => { + if (flushTimerRef.current !== null) return; + flushTimerRef.current = window.setTimeout(async () => { + flushTimerRef.current = null; + await flushInput(); + }, 30); + }; + + // ========== xterm Initialization ========== + + useEffect(() => { + if (!terminalHostRef.current) return; + if (xtermRef.current) return; + + const term = new XTerm({ + convertEol: true, + cursorBlink: true, + fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', + fontSize: 12, + scrollback: 5000, + }); + const fitAddon = new FitAddon(); + term.loadAddon(fitAddon); + term.open(terminalHostRef.current); + fitAddon.fit(); + + // Forward keystrokes to backend (batched) + term.onData((data) => { + if (!activeSessionKeyRef.current) return; + pendingInputRef.current += data; + scheduleFlush(); + }); + + xtermRef.current = term; + fitAddonRef.current = fitAddon; + + return () => { + try { + term.dispose(); + } finally { + xtermRef.current = null; + fitAddonRef.current = null; + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // ========== Attach to Active Session ========== + + useEffect(() => { + const term = xtermRef.current; + const fitAddon = fitAddonRef.current; + if (!term || !fitAddon) return; + + lastChunkIndexRef.current = 0; + term.reset(); + term.clear(); + + if (!activeTerminalId) return; + clearOutput(activeTerminalId); + + fetchCliSessionBuffer(activeTerminalId, projectPath || undefined) + .then(({ buffer }) => { + setBuffer(activeTerminalId, buffer || ''); + }) + .catch(() => { + // ignore + }) + .finally(() => { + fitAddon.fit(); + }); + }, [activeTerminalId, projectPath, setBuffer, clearOutput]); + + // ========== Stream Output Chunks ========== + + useEffect(() => { + const term = xtermRef.current; + if (!term) return; + if (!activeTerminalId) return; + + const chunks = outputChunks[activeTerminalId] ?? []; + const start = lastChunkIndexRef.current; + if (start >= chunks.length) return; + + for (let i = start; i < chunks.length; i++) { + term.write(chunks[i].data); + } + lastChunkIndexRef.current = chunks.length; + }, [outputChunks, activeTerminalId]); + + // ========== Resize Observer ========== + + useEffect(() => { + const host = terminalHostRef.current; + const term = xtermRef.current; + const fitAddon = fitAddonRef.current; + if (!host || !term || !fitAddon) return; + + const resize = () => { + fitAddon.fit(); + const sessionKey = activeSessionKeyRef.current; + if (sessionKey) { + void (async () => { + try { + await resizeCliSession(sessionKey, { cols: term.cols, rows: term.rows }, projectPath || undefined); + } catch { + // ignore + } + })(); + } + }; + + const ro = new ResizeObserver(resize); + ro.observe(host); + return () => ro.disconnect(); + }, [projectPath]); + + // ========== Execute Command ========== + + const handleExecute = async () => { + if (!activeTerminalId) return; + if (!prompt.trim()) return; + setIsExecuting(true); + setError(null); + try { + await executeInCliSession(activeTerminalId, { + tool: activeSession?.tool || 'claude', + prompt: prompt.trim(), + mode: 'analysis', + category: 'user', + }, projectPath || undefined); + setPrompt(''); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setIsExecuting(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + // Ctrl+Enter or Cmd+Enter to execute + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + e.preventDefault(); + void handleExecute(); + } + }; + + // ========== Render ========== + + return ( +
+ {/* Header */} +
+
+

+ {activeSession + ? `${activeSession.tool || 'cli'} - ${activeSession.sessionKey}` + : 'Terminal Panel'} +

+ {activeSession?.tool && ( + + {activeSession.workingDir} + + )} +
+ +
+ + {/* Tabs */} + setPanelView(v as PanelView)} + className="flex-1 flex flex-col min-h-0" + > +
+ + Terminal + Queue + +
+ + {/* Terminal View */} + + {activeTerminalId ? ( +
+ {/* xterm container */} +
+
+
+ + {/* Command input area */} +
+ {error && ( +
{error}
+ )} +
+