-
+
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/CliStreamMonitorNew.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/CliStreamMonitorNew.tsx
index 4b4da222..e87b1152 100644
--- a/ccw/frontend/src/components/shared/CliStreamMonitor/CliStreamMonitorNew.tsx
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/CliStreamMonitorNew.tsx
@@ -3,7 +3,7 @@
// ========================================
// Redesigned CLI streaming monitor with smart parsing and message-based layout
-import { useEffect, useState, useCallback, useMemo } from 'react';
+import { useEffect, useState, useCallback, useMemo, useRef } from 'react';
import { useIntl } from 'react-intl';
import {
Terminal,
@@ -220,9 +220,14 @@ export function CliStreamMonitorNew({ isOpen, onClose }: CliStreamMonitorNewProp
// WebSocket last message
const lastMessage = useNotificationStore(selectWsLastMessage);
+ // Track last processed WebSocket message to prevent duplicate processing
+ const lastProcessedMsgRef = useRef(null);
+
// Handle WebSocket messages (same as original)
useEffect(() => {
- if (!lastMessage) return;
+ // Skip if no message or same message already processed (prevents React strict mode double-execution)
+ if (!lastMessage || lastMessage === lastProcessedMsgRef.current) return;
+ lastProcessedMsgRef.current = lastMessage;
const { type, payload } = lastMessage;
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/components/ExecutionTab.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/components/ExecutionTab.tsx
index 52354622..862b2549 100644
--- a/ccw/frontend/src/components/shared/CliStreamMonitor/components/ExecutionTab.tsx
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/components/ExecutionTab.tsx
@@ -58,13 +58,21 @@ export function ExecutionTab({ execution, isActive, onClick, onClose }: Executio
{/* Close button - show on hover */}
-
+
);
}
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx b/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx
index 2c9cba8c..d4468146 100644
--- a/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx
+++ b/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx
@@ -14,7 +14,9 @@ import {
Search,
ArrowDownToLine,
Trash2,
+ ExternalLink,
} from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
@@ -196,6 +198,7 @@ export interface CliStreamMonitorProps {
export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
const { formatMessage } = useIntl();
+ const navigate = useNavigate();
const logsEndRef = useRef(null);
const logsContainerRef = useRef(null);
const [searchQuery, setSearchQuery] = useState('');
@@ -206,6 +209,9 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
// Track last output length to detect new output
const lastOutputLengthRef = useRef>({});
+ // Track last processed WebSocket message to prevent duplicate processing
+ const lastProcessedMsgRef = useRef(null);
+
// Store state
const executions = useCliStreamStore((state) => state.executions);
const currentExecutionId = useCliStreamStore((state) => state.currentExecutionId);
@@ -222,7 +228,9 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
// Handle WebSocket messages for CLI stream
useEffect(() => {
- if (!lastMessage) return;
+ // Skip if no message or same message already processed (prevents React strict mode double-execution)
+ if (!lastMessage || lastMessage === lastProcessedMsgRef.current) return;
+ lastProcessedMsgRef.current = lastMessage;
const { type, payload } = lastMessage;
@@ -377,6 +385,15 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
setCurrentExecution(null);
}, [markExecutionClosedByUser, removeExecution, executions, setCurrentExecution]);
+ // Open in full page viewer
+ const handlePopOut = useCallback(() => {
+ const url = currentExecutionId
+ ? `/cli-viewer?executionId=${currentExecutionId}`
+ : '/cli-viewer';
+ navigate(url);
+ onClose();
+ }, [currentExecutionId, navigate, onClose]);
+
// ESC key to close
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
@@ -507,6 +524,14 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
)}
+
)}
- {isUserScrolling && filteredOutput.length > 0 && (
-
- )}
)}
diff --git a/ccw/frontend/src/components/shared/NavGroup.tsx b/ccw/frontend/src/components/shared/NavGroup.tsx
index 5567bd7c..e174ec80 100644
--- a/ccw/frontend/src/components/shared/NavGroup.tsx
+++ b/ccw/frontend/src/components/shared/NavGroup.tsx
@@ -54,9 +54,10 @@ export function NavGroup({
{items.map((item) => {
const ItemIcon = item.icon;
const [basePath] = item.path.split('?');
+ // More precise matching: exact match or basePath followed by '/' to avoid parent/child conflicts
const isActive =
location.pathname === basePath ||
- (basePath !== '/' && location.pathname.startsWith(basePath));
+ (basePath !== '/' && location.pathname.startsWith(basePath + '/'));
return (