mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-12 17:21:19 +08:00
- Introduced tests for stale running fallback in CLI watch functionality to ensure proper handling of saved conversations. - Added compatibility tests for CodexLens CLI to verify index initialization despite compatibility conflicts. - Implemented tests for Smart Search MCP usage to validate default settings and path handling. - Created tests for UV Manager to ensure Python preference handling works as expected. - Added a detailed guide for CCW/Codex commands and skills, covering core commands, execution modes, and templates.
169 lines
5.4 KiB
TypeScript
169 lines
5.4 KiB
TypeScript
// ========================================
|
|
// CLI Viewer Page
|
|
// ========================================
|
|
// Multi-pane CLI output viewer with configurable layouts
|
|
// Integrates with viewerStore for state management
|
|
// Includes WebSocket integration and execution recovery
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { useSearchParams } from 'react-router-dom';
|
|
import { LayoutContainer, CliViewerToolbar } from '@/components/cli-viewer';
|
|
import {
|
|
useViewerStore,
|
|
useViewerLayout,
|
|
useViewerPanes,
|
|
useFocusedPaneId,
|
|
type AllotmentLayout,
|
|
} from '@/stores/viewerStore';
|
|
import { useCliStreamStore } from '@/stores/cliStreamStore';
|
|
import { useActiveCliExecutions } from '@/hooks/useActiveCliExecutions';
|
|
import { useCliStreamWebSocket } from '@/hooks/useCliStreamWebSocket';
|
|
import { getStaleViewerTabs } from './cliViewerPage.utils';
|
|
|
|
// ========================================
|
|
// Constants
|
|
// ========================================
|
|
|
|
const DEFAULT_LAYOUT = 'split-h' as const;
|
|
|
|
// ========================================
|
|
// Helper Functions
|
|
// ========================================
|
|
|
|
/**
|
|
* Count total panes in layout
|
|
*/
|
|
function countPanes(layout: AllotmentLayout): number {
|
|
let count = 0;
|
|
const traverse = (children: (string | AllotmentLayout)[]) => {
|
|
for (const child of children) {
|
|
if (typeof child === 'string') {
|
|
count++;
|
|
} else {
|
|
traverse(child.children);
|
|
}
|
|
}
|
|
};
|
|
traverse(layout.children);
|
|
return count;
|
|
}
|
|
|
|
// ========================================
|
|
// Main Component
|
|
// ========================================
|
|
|
|
export function CliViewerPage() {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
// Fullscreen state
|
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
|
|
// Store hooks
|
|
const layout = useViewerLayout();
|
|
const panes = useViewerPanes();
|
|
const focusedPaneId = useFocusedPaneId();
|
|
const { initializeDefaultLayout, addTab, removeTab } = useViewerStore();
|
|
|
|
// CLI Stream Store hooks
|
|
const executions = useCliStreamStore((state) => state.executions);
|
|
|
|
// Active execution sync from server
|
|
const { isLoading: isSyncing, isFetching: isRefreshing } = useActiveCliExecutions(true);
|
|
|
|
// CENTRALIZED WebSocket handler - processes each message only ONCE globally
|
|
useCliStreamWebSocket();
|
|
|
|
// Auto-add new executions as tabs, distributing across available panes
|
|
const addedExecutionsRef = useRef<Set<string>>(new Set());
|
|
|
|
// FIX-001: Initialize addedExecutionsRef with existing tab executionIds on mount
|
|
// This prevents duplicate tabs from being added after page refresh
|
|
useEffect(() => {
|
|
// Extract executionIds from all existing tabs in all panes
|
|
const existingExecutionIds = Object.values(panes).flatMap((pane) =>
|
|
pane.tabs.map((tab) => tab.executionId)
|
|
);
|
|
existingExecutionIds.forEach((id) => addedExecutionsRef.current.add(id));
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []); // Empty deps - only run once on mount
|
|
|
|
useEffect(() => {
|
|
const paneIds = Object.keys(panes);
|
|
if (paneIds.length === 0) return;
|
|
|
|
const storeAddTab = useViewerStore.getState().addTab;
|
|
const newExecutionIds = Object.keys(executions).filter(
|
|
(id) => !addedExecutionsRef.current.has(id)
|
|
);
|
|
|
|
if (newExecutionIds.length === 0) return;
|
|
|
|
newExecutionIds.forEach((executionId, index) => {
|
|
addedExecutionsRef.current.add(executionId);
|
|
const exec = executions[executionId];
|
|
const toolShort = exec.tool.split('-')[0];
|
|
const targetPaneId = paneIds[index % paneIds.length];
|
|
storeAddTab(targetPaneId, executionId, `${toolShort} (${exec.mode})`);
|
|
});
|
|
}, [executions, panes]);
|
|
|
|
useEffect(() => {
|
|
if (isSyncing || isRefreshing) return;
|
|
|
|
const staleTabs = getStaleViewerTabs(panes, executions);
|
|
if (staleTabs.length === 0) return;
|
|
|
|
staleTabs.forEach(({ paneId, tabId, executionId }) => {
|
|
addedExecutionsRef.current.delete(executionId);
|
|
removeTab(paneId, tabId);
|
|
});
|
|
}, [executions, isRefreshing, isSyncing, panes, removeTab]);
|
|
|
|
// Initialize layout if empty
|
|
useEffect(() => {
|
|
const paneCount = countPanes(layout);
|
|
if (paneCount === 0) {
|
|
initializeDefaultLayout(DEFAULT_LAYOUT);
|
|
}
|
|
}, [layout, initializeDefaultLayout]);
|
|
|
|
// Handle executionId from URL params
|
|
useEffect(() => {
|
|
const executionId = searchParams.get('executionId');
|
|
if (executionId && focusedPaneId) {
|
|
addTab(focusedPaneId, executionId, `Execution ${executionId.slice(0, 8)}`);
|
|
setSearchParams((prev) => {
|
|
const newParams = new URLSearchParams(prev);
|
|
newParams.delete('executionId');
|
|
return newParams;
|
|
});
|
|
}
|
|
}, [searchParams, focusedPaneId, addTab, setSearchParams]);
|
|
|
|
// Toggle fullscreen handler
|
|
const handleToggleFullscreen = () => {
|
|
setIsFullscreen((prev) => !prev);
|
|
};
|
|
|
|
return (
|
|
<div className="h-full flex flex-col">
|
|
{/* ======================================== */}
|
|
{/* Toolbar */}
|
|
{/* ======================================== */}
|
|
<CliViewerToolbar
|
|
isFullscreen={isFullscreen}
|
|
onToggleFullscreen={handleToggleFullscreen}
|
|
/>
|
|
|
|
{/* ======================================== */}
|
|
{/* Layout Container */}
|
|
{/* ======================================== */}
|
|
<div className="flex-1 min-h-0 bg-background">
|
|
<LayoutContainer />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default CliViewerPage;
|