mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: 添加执行加载状态和全局执行ID去重,优化CLI视图和MCP配置获取
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
import { useCliStreamStore, type CliExecutionState, type CliOutputLine } from '@/stores/cliStreamStore';
|
||||
import { MonitorBody } from '@/components/shared/CliStreamMonitor/MonitorBody';
|
||||
import { MessageRenderer } from '@/components/shared/CliStreamMonitor/MessageRenderer';
|
||||
import { useActiveCliExecutions } from '@/hooks/useActiveCliExecutions';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
@@ -68,6 +69,25 @@ function ExecutionNotFoundState({ executionId }: { executionId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX-002: Loading state while syncing executions from server
|
||||
* Shown after page refresh while execution data is being recovered
|
||||
*/
|
||||
function ExecutionLoadingState() {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-muted-foreground gap-4">
|
||||
<Loader2 className="h-8 w-8 animate-spin opacity-50" />
|
||||
<div className="text-center">
|
||||
<p className="text-sm">
|
||||
{formatMessage({ id: 'cliViewer.syncingExecution', defaultMessage: 'Syncing execution data...' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Single output line component with type-based styling
|
||||
*/
|
||||
@@ -157,10 +177,14 @@ function CliOutputDisplay({ execution, executionId }: { execution: CliExecutionS
|
||||
export function ContentArea({ paneId, className }: ContentAreaProps) {
|
||||
// Get active tab using the selector
|
||||
const activeTab = useViewerStore((state) => selectActiveTab(state, paneId));
|
||||
|
||||
|
||||
// Get execution data from cliStreamStore
|
||||
const executions = useCliStreamStore((state) => state.executions);
|
||||
|
||||
|
||||
// FIX-002: Get loading state from useActiveCliExecutions
|
||||
// This helps distinguish between "not found" and "still loading"
|
||||
const { isLoading: isSyncing } = useActiveCliExecutions(true);
|
||||
|
||||
const execution = useMemo(() => {
|
||||
if (!activeTab?.executionId) return null;
|
||||
return executions[activeTab.executionId] || null;
|
||||
@@ -173,14 +197,19 @@ export function ContentArea({ paneId, className }: ContentAreaProps) {
|
||||
return <EmptyTabState />;
|
||||
}
|
||||
|
||||
// No execution data found
|
||||
// FIX-002: Show loading state while syncing if execution not yet available
|
||||
if (!execution && isSyncing) {
|
||||
return <ExecutionLoadingState />;
|
||||
}
|
||||
|
||||
// No execution data found (after sync completed)
|
||||
if (!execution) {
|
||||
return <ExecutionNotFoundState executionId={activeTab.executionId} />;
|
||||
}
|
||||
|
||||
// Show CLI output
|
||||
return <CliOutputDisplay execution={execution} executionId={activeTab.executionId} />;
|
||||
}, [activeTab, execution]);
|
||||
}, [activeTab, execution, isSyncing]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -3708,7 +3708,7 @@ function buildCcwMcpServerConfig(config: {
|
||||
/**
|
||||
* Fetch CCW Tools MCP configuration by checking if ccw-tools server exists
|
||||
*/
|
||||
export async function fetchCcwMcpConfig(): Promise<CcwMcpConfig> {
|
||||
export async function fetchCcwMcpConfig(currentProjectPath?: string): Promise<CcwMcpConfig> {
|
||||
try {
|
||||
const config = await fetchMcpConfig();
|
||||
|
||||
@@ -3724,13 +3724,27 @@ export async function fetchCcwMcpConfig(): Promise<CcwMcpConfig> {
|
||||
ccwServer = config.userServers['ccw-tools'];
|
||||
}
|
||||
|
||||
// Check project servers
|
||||
// Check project servers - only check current project if specified
|
||||
if (config.projects) {
|
||||
for (const proj of Object.values(config.projects)) {
|
||||
if (proj.mcpServers?.['ccw-tools']) {
|
||||
installedScopes.push('project');
|
||||
if (!ccwServer) ccwServer = proj.mcpServers['ccw-tools'];
|
||||
break;
|
||||
if (currentProjectPath) {
|
||||
// Normalize path for comparison (forward slashes)
|
||||
const normalizedCurrent = currentProjectPath.replace(/\\/g, '/');
|
||||
for (const [key, proj] of Object.entries(config.projects)) {
|
||||
const normalizedKey = key.replace(/\\/g, '/');
|
||||
if (normalizedKey === normalizedCurrent && proj.mcpServers?.['ccw-tools']) {
|
||||
installedScopes.push('project');
|
||||
if (!ccwServer) ccwServer = proj.mcpServers['ccw-tools'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: check all projects (legacy behavior)
|
||||
for (const proj of Object.values(config.projects)) {
|
||||
if (proj.mcpServers?.['ccw-tools']) {
|
||||
installedScopes.push('project');
|
||||
if (!ccwServer) ccwServer = proj.mcpServers['ccw-tools'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +215,18 @@ export function CliViewerPage() {
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -279,10 +279,12 @@ export function McpManagerPage() {
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
});
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
// Fetch CCW Tools MCP configuration (Claude mode only)
|
||||
const ccwMcpQuery = useQuery({
|
||||
queryKey: ['ccwMcpConfig'],
|
||||
queryFn: fetchCcwMcpConfig,
|
||||
queryKey: ['ccwMcpConfig', projectPath],
|
||||
queryFn: () => fetchCcwMcpConfig(projectPath ?? undefined),
|
||||
enabled: cliMode === 'claude',
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
@@ -382,18 +384,20 @@ export function McpManagerPage() {
|
||||
installedScopes: [] as ('global' | 'project')[],
|
||||
};
|
||||
|
||||
const ccwMcpQueryKey = ['ccwMcpConfig', projectPath];
|
||||
|
||||
const handleToggleCcwTool = async (tool: string, enabled: boolean) => {
|
||||
// Read latest from cache to avoid stale closures
|
||||
const currentConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfig']) ?? ccwConfig;
|
||||
const currentConfig = queryClient.getQueryData<CcwMcpConfig>(ccwMcpQueryKey) ?? ccwConfig;
|
||||
const currentTools = currentConfig.enabledTools;
|
||||
const previousConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfig']);
|
||||
const previousConfig = queryClient.getQueryData<CcwMcpConfig>(ccwMcpQueryKey);
|
||||
|
||||
const updatedTools = enabled
|
||||
? (currentTools.includes(tool) ? currentTools : [...currentTools, tool])
|
||||
: currentTools.filter((t) => t !== tool);
|
||||
|
||||
// Optimistic cache update for immediate UI response
|
||||
queryClient.setQueryData(['ccwMcpConfig'], (old: CcwMcpConfig | undefined) => {
|
||||
queryClient.setQueryData(ccwMcpQueryKey, (old: CcwMcpConfig | undefined) => {
|
||||
if (!old) return old;
|
||||
return { ...old, enabledTools: updatedTools };
|
||||
});
|
||||
@@ -402,18 +406,18 @@ export function McpManagerPage() {
|
||||
await updateCcwConfig({ ...currentConfig, enabledTools: updatedTools });
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle CCW tool:', error);
|
||||
queryClient.setQueryData(['ccwMcpConfig'], previousConfig);
|
||||
queryClient.setQueryData(ccwMcpQueryKey, previousConfig);
|
||||
}
|
||||
ccwMcpQuery.refetch();
|
||||
};
|
||||
|
||||
const handleUpdateCcwConfig = async (config: Partial<CcwMcpConfig>) => {
|
||||
// Read BEFORE optimistic update to capture actual server state
|
||||
const currentConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfig']) ?? ccwConfig;
|
||||
const previousConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfig']);
|
||||
const currentConfig = queryClient.getQueryData<CcwMcpConfig>(ccwMcpQueryKey) ?? ccwConfig;
|
||||
const previousConfig = queryClient.getQueryData<CcwMcpConfig>(ccwMcpQueryKey);
|
||||
|
||||
// Optimistic cache update for immediate UI response
|
||||
queryClient.setQueryData(['ccwMcpConfig'], (old: CcwMcpConfig | undefined) => {
|
||||
queryClient.setQueryData(ccwMcpQueryKey, (old: CcwMcpConfig | undefined) => {
|
||||
if (!old) return old;
|
||||
return { ...old, ...config };
|
||||
});
|
||||
@@ -422,7 +426,7 @@ export function McpManagerPage() {
|
||||
await updateCcwConfig({ ...currentConfig, ...config });
|
||||
} catch (error) {
|
||||
console.error('Failed to update CCW config:', error);
|
||||
queryClient.setQueryData(['ccwMcpConfig'], previousConfig);
|
||||
queryClient.setQueryData(ccwMcpQueryKey, previousConfig);
|
||||
}
|
||||
ccwMcpQuery.refetch();
|
||||
};
|
||||
@@ -431,8 +435,6 @@ export function McpManagerPage() {
|
||||
ccwMcpQuery.refetch();
|
||||
};
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
// Build conflict map for quick lookup
|
||||
const conflictMap = useMemo(() => {
|
||||
const map = new Map<string, McpServerConflict>();
|
||||
|
||||
@@ -491,6 +491,31 @@ export const useViewerStore = create<ViewerState>()(
|
||||
return existingTab.id;
|
||||
}
|
||||
|
||||
// FIX-004: Global executionId deduplication (VSCode parity)
|
||||
// Check all panes for existing tab with same executionId
|
||||
for (const [pid, p] of Object.entries(state.panes)) {
|
||||
if (pid === paneId) continue; // Already checked above
|
||||
const existingInOtherPane = p.tabs.find((t) => t.executionId === executionId);
|
||||
if (existingInOtherPane) {
|
||||
// Activate the existing tab in its pane and focus that pane
|
||||
set(
|
||||
{
|
||||
panes: {
|
||||
...state.panes,
|
||||
[pid]: {
|
||||
...p,
|
||||
activeTabId: existingInOtherPane.id,
|
||||
},
|
||||
},
|
||||
focusedPaneId: pid,
|
||||
},
|
||||
false,
|
||||
'viewer/addTab-existing-global'
|
||||
);
|
||||
return existingInOtherPane.id;
|
||||
}
|
||||
}
|
||||
|
||||
const newTabId = generateTabId(state.nextTabIdCounter);
|
||||
const maxOrder = pane.tabs.reduce((max, t) => Math.max(max, t.order), 0);
|
||||
|
||||
@@ -575,6 +600,21 @@ export const useViewerStore = create<ViewerState>()(
|
||||
false,
|
||||
'viewer/removeTab'
|
||||
);
|
||||
|
||||
// FIX-003: Auto-cleanup empty panes after tab removal (VSCode parity)
|
||||
if (newTabs.length === 0) {
|
||||
const allPaneIds = getAllPaneIds(get().layout);
|
||||
// Don't remove if it's the last pane
|
||||
if (allPaneIds.length > 1) {
|
||||
// Use queueMicrotask to avoid state mutation during current transaction
|
||||
queueMicrotask(() => {
|
||||
const currentState = get();
|
||||
if (currentState.panes[paneId]?.tabs.length === 0) {
|
||||
currentState.removePane(paneId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setActiveTab: (paneId: PaneId, tabId: TabId) => {
|
||||
@@ -720,6 +760,22 @@ export const useViewerStore = create<ViewerState>()(
|
||||
false,
|
||||
'viewer/moveTab'
|
||||
);
|
||||
|
||||
// FIX-003: Auto-cleanup empty panes after tab movement (VSCode parity)
|
||||
// Only cleanup when moving to a different pane and source becomes empty
|
||||
if (sourcePaneId !== targetPaneId && newSourceTabs.length === 0) {
|
||||
const allPaneIds = getAllPaneIds(get().layout);
|
||||
// Don't remove if it's the last pane
|
||||
if (allPaneIds.length > 1) {
|
||||
// Use queueMicrotask to avoid state mutation during current transaction
|
||||
queueMicrotask(() => {
|
||||
const currentState = get();
|
||||
if (currentState.panes[sourcePaneId]?.tabs.length === 0) {
|
||||
currentState.removePane(sourcePaneId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
togglePinTab: (tabId: TabId) => {
|
||||
|
||||
Reference in New Issue
Block a user