diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 31b0204f90f378b3c4550e997686ae87d8654f58..7a29f57e080b2300a58b297c40d8a33b632756fe 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -51,7 +51,7 @@ import { } from "../session-logic"; import { isScrollContainerNearBottom } from "../chat-scroll"; import { useStore } from "../store"; -import type { ChatImageAttachment } from "../types"; +import { MAX_THREAD_TERMINAL_COUNT, type ChatImageAttachment } from "../types"; import BranchToolbar from "./BranchToolbar"; import GitActionsControl from "./GitActionsControl"; import { @@ -294,6 +294,8 @@ export default function ChatView() { (activeThread.messages.length > 0 || (activeThread.session !== null && activeThread.session.status !== "closed")), ); + const hasReachedTerminalLimit = + (activeThread?.terminalIds.length ?? 0) >= MAX_THREAD_TERMINAL_COUNT; const revokePreviewUrls = useCallback((images: Array<{ previewUrl?: string }>) => { for (const image of images) { @@ -322,23 +324,23 @@ export default function ChatView() { }); }, [activeThread?.terminalOpen, activeThreadId, dispatch]); const splitTerminal = useCallback(() => { - if (!activeThreadId) return; + if (!activeThreadId || hasReachedTerminalLimit) return; dispatch({ type: "SPLIT_THREAD_TERMINAL", threadId: activeThreadId, terminalId: `terminal-${crypto.randomUUID()}`, }); setTerminalFocusRequestId((value) => value + 1); - }, [activeThreadId, dispatch]); + }, [activeThreadId, dispatch, hasReachedTerminalLimit]); const createNewTerminal = useCallback(() => { - if (!activeThreadId) return; + if (!activeThreadId || hasReachedTerminalLimit) return; dispatch({ type: "NEW_THREAD_TERMINAL", threadId: activeThreadId, terminalId: `terminal-${crypto.randomUUID()}`, }); setTerminalFocusRequestId((value) => value + 1); - }, [activeThreadId, dispatch]); + }, [activeThreadId, dispatch, hasReachedTerminalLimit]); const activateTerminal = useCallback( (terminalId: string) => { if (!activeThreadId) return; diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 1c35459a9b11fab296db05e6cf7485f008df48d1..eaf05571cb868acf825b5ff5a9bf5131dd9b70b2 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -22,6 +22,7 @@ import { isTerminalClearShortcut, terminalNavigationShortcutData } from "../keyb import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID, + MAX_THREAD_TERMINAL_COUNT, type ThreadTerminalGroup, } from "../types"; @@ -581,6 +582,7 @@ export default function ThreadTerminalDrawer({ const showGroupHeaders = resolvedTerminalGroups.length > 1 || resolvedTerminalGroups.some((terminalGroup) => terminalGroup.terminalIds.length > 1); + const hasReachedTerminalLimit = normalizedTerminalIds.length >= MAX_THREAD_TERMINAL_COUNT; const terminalLabelById = useMemo( () => new Map( @@ -588,9 +590,23 @@ export default function ThreadTerminalDrawer({ ), [normalizedTerminalIds], ); + const splitTerminalActionLabel = hasReachedTerminalLimit + ? `Split Terminal (max ${MAX_THREAD_TERMINAL_COUNT})` + : (splitShortcutLabel ? `Split Terminal (${splitShortcutLabel})` : "Split Terminal"); + const newTerminalActionLabel = hasReachedTerminalLimit + ? `New Terminal (max ${MAX_THREAD_TERMINAL_COUNT})` + : (newShortcutLabel ? `New Terminal (${newShortcutLabel})` : "New Terminal"); const closeTerminalActionLabel = closeShortcutLabel ? `Close Terminal (${closeShortcutLabel})` : "Close Terminal"; + const onSplitTerminalAction = useCallback(() => { + if (hasReachedTerminalLimit) return; + onSplitTerminal(); + }, [hasReachedTerminalLimit, onSplitTerminal]); + const onNewTerminalAction = useCallback(() => { + if (hasReachedTerminalLimit) return; + onNewTerminal(); + }, [hasReachedTerminalLimit, onNewTerminal]); useEffect(() => { onHeightChangeRef.current = onHeightChange; @@ -685,7 +701,7 @@ export default function ThreadTerminalDrawer({ return (