diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index 084e835ac81070cb7f13809d4d1db7f62292ea35..cb7dea0ce20ab26b3cbd773d53f3b6b9e4db3fa8 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -20,6 +20,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } import { render } from "vitest-browser-react"; import { useComposerDraftStore } from "../composerDraftStore"; +import { isMacPlatform } from "../lib/utils"; import { getRouter } from "../router"; import { useStore } from "../store"; import { estimateTimelineMessageHeight } from "./timelineHeight"; @@ -1062,6 +1063,130 @@ describe("ChatView timeline estimator parity (full app)", () => { } }); + it("creates a new thread from the global chat.new shortcut", async () => { + const mounted = await mountChatView({ + viewport: DEFAULT_VIEWPORT, + snapshot: createSnapshotForTargetUser({ + targetMessageId: "msg-user-chat-shortcut-test" as MessageId, + targetText: "chat shortcut test", + }), + configureFixture: (nextFixture) => { + nextFixture.serverConfig = { + ...nextFixture.serverConfig, + keybindings: [ + { + command: "chat.new", + shortcut: { + key: "o", + metaKey: false, + ctrlKey: false, + shiftKey: true, + altKey: false, + modKey: true, + }, + whenAst: { + type: "not", + node: { type: "identifier", name: "terminalFocus" }, + }, + }, + ], + }; + }, + }); + + try { + const useMetaForMod = isMacPlatform(navigator.platform); + window.dispatchEvent( + new KeyboardEvent("keydown", { + key: "o", + shiftKey: true, + metaKey: useMetaForMod, + ctrlKey: !useMetaForMod, + bubbles: true, + cancelable: true, + }), + ); + + await waitForURL( + mounted.router, + (path) => UUID_ROUTE_RE.test(path), + "Route should have changed to a new draft thread UUID from the shortcut.", + ); + } finally { + await mounted.cleanup(); + } + }); + + it("creates a fresh draft after the previous draft thread is promoted", async () => { + const mounted = await mountChatView({ + viewport: DEFAULT_VIEWPORT, + snapshot: createSnapshotForTargetUser({ + targetMessageId: "msg-user-promoted-draft-shortcut-test" as MessageId, + targetText: "promoted draft shortcut test", + }), + configureFixture: (nextFixture) => { + nextFixture.serverConfig = { + ...nextFixture.serverConfig, + keybindings: [ + { + command: "chat.new", + shortcut: { + key: "o", + metaKey: false, + ctrlKey: false, + shiftKey: true, + altKey: false, + modKey: true, + }, + whenAst: { + type: "not", + node: { type: "identifier", name: "terminalFocus" }, + }, + }, + ], + }; + }, + }); + + try { + const newThreadButton = page.getByTestId("new-thread-button"); + await expect.element(newThreadButton).toBeInTheDocument(); + await newThreadButton.click(); + + const promotedThreadPath = await waitForURL( + mounted.router, + (path) => UUID_ROUTE_RE.test(path), + "Route should have changed to a promoted draft thread UUID.", + ); + const promotedThreadId = promotedThreadPath.slice(1) as ThreadId; + + const { syncServerReadModel } = useStore.getState(); + syncServerReadModel(addThreadToSnapshot(fixture.snapshot, promotedThreadId)); + useComposerDraftStore.getState().clearDraftThread(promotedThreadId); + + const useMetaForMod = isMacPlatform(navigator.platform); + window.dispatchEvent( + new KeyboardEvent("keydown", { + key: "o", + shiftKey: true, + metaKey: useMetaForMod, + ctrlKey: !useMetaForMod, + bubbles: true, + cancelable: true, + }), + ); + + const freshThreadPath = await waitForURL( + mounted.router, + (path) => UUID_ROUTE_RE.test(path) && path !== promotedThreadPath, + "Shortcut should create a fresh draft instead of reusing the promoted thread.", + ); + expect(freshThreadPath).not.toBe(promotedThreadPath); + } finally { + await mounted.cleanup(); + } + }); + it("keeps long proposed plans lightweight until the user expands them", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index f047f384ea208dac334163ecf1c620e14d599fee..327789db650a61bec77dcc5cf44f12a9eca280a0 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -76,7 +76,7 @@ import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, DEFAULT_THREAD_TERMINAL_ID, - MAX_THREAD_TERMINAL_COUNT, + MAX_TERMINALS_PER_GROUP, type ChatMessage, type TurnDiffSummary, } from "../types"; @@ -1037,7 +1037,16 @@ export default function ChatView({ threadId }: ChatViewProps) { (activeThread.messages.length > 0 || (activeThread.session !== null && activeThread.session.status !== "closed")), ); - const hasReachedTerminalLimit = terminalState.terminalIds.length >= MAX_THREAD_TERMINAL_COUNT; + const activeTerminalGroup = + terminalState.terminalGroups.find( + (group) => group.id === terminalState.activeTerminalGroupId, + ) ?? + terminalState.terminalGroups.find((group) => + group.terminalIds.includes(terminalState.activeTerminalId), + ) ?? + null; + const hasReachedSplitLimit = + (activeTerminalGroup?.terminalIds.length ?? 0) >= MAX_TERMINALS_PER_GROUP; const setThreadError = useCallback( (targetThreadId: ThreadId | null, error: string | null) => { if (!targetThreadId) return; @@ -1085,17 +1094,17 @@ export default function ChatView({ threadId }: ChatViewProps) { setTerminalOpen(!terminalState.terminalOpen); }, [activeThreadId, setTerminalOpen, terminalState.terminalOpen]); const splitTerminal = useCallback(() => { - if (!activeThreadId || hasReachedTerminalLimit) return; + if (!activeThreadId || hasReachedSplitLimit) return; const terminalId = `terminal-${randomUUID()}`; storeSplitTerminal(activeThreadId, terminalId); setTerminalFocusRequestId((value) => value + 1); - }, [activeThreadId, storeSplitTerminal, hasReachedTerminalLimit]); + }, [activeThreadId, hasReachedSplitLimit, storeSplitTerminal]); const createNewTerminal = useCallback(() => { - if (!activeThreadId || hasReachedTerminalLimit) return; + if (!activeThreadId) return; const terminalId = `terminal-${randomUUID()}`; storeNewTerminal(activeThreadId, terminalId); setTerminalFocusRequestId((value) => value + 1); - }, [activeThreadId, storeNewTerminal, hasReachedTerminalLimit]); + }, [activeThreadId, storeNewTerminal]); const activateTerminal = useCallback( (terminalId: string) => { if (!activeThreadId) return; @@ -1162,8 +1171,7 @@ export default function ChatView({ threadId }: ChatViewProps) { DEFAULT_THREAD_TERMINAL_ID; const isBaseTerminalBusy = terminalState.runningTerminalIds.includes(baseTerminalId); const wantsNewTerminal = Boolean(options?.preferNewTerminal) || isBaseTerminalBusy; - const shouldCreateNewTerminal = - wantsNewTerminal && terminalState.terminalIds.length < MAX_THREAD_TERMINAL_COUNT; + const shouldCreateNewTerminal = wantsNewTerminal; const targetTerminalId = shouldCreateNewTerminal ? `terminal-${randomUUID()}` : baseTerminalId; diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 634d91e9dcfbf4335adb5c7a979555afbece712a..f35f8782691b5dcec35fedaaf23fb2ba3506700d 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { hasUnseenCompletion, + resolveThreadRowClassName, resolveThreadStatusPill, shouldClearThreadSelectionOnMouseDown, } from "./Sidebar.logic"; @@ -154,3 +155,27 @@ describe("resolveThreadStatusPill", () => { ).toMatchObject({ label: "Completed", pulse: false }); }); }); + +describe("resolveThreadRowClassName", () => { + it("uses the darker selected palette when a thread is both selected and active", () => { + const className = resolveThreadRowClassName({ isActive: true, isSelected: true }); + expect(className).toContain("bg-primary/22"); + expect(className).toContain("hover:bg-primary/26"); + expect(className).toContain("dark:bg-primary/30"); + expect(className).not.toContain("bg-accent/85"); + }); + + it("uses selected hover colors for selected threads", () => { + const className = resolveThreadRowClassName({ isActive: false, isSelected: true }); + expect(className).toContain("bg-primary/15"); + expect(className).toContain("hover:bg-primary/19"); + expect(className).toContain("dark:bg-primary/22"); + expect(className).not.toContain("hover:bg-accent"); + }); + + it("keeps the accent palette for active-only threads", () => { + const className = resolveThreadRowClassName({ isActive: true, isSelected: false }); + expect(className).toContain("bg-accent/85"); + expect(className).toContain("hover:bg-accent"); + }); +}); diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index f1afa0f6462c531018ecf984ff04120014aabe5c..8acbed63a9a422f38fce188deadde21c2fc8e4a8 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -1,4 +1,5 @@ import type { Thread } from "../types"; +import { cn } from "../lib/utils"; import { findLatestProposedPlan, isLatestTurnSettled } from "../session-logic"; export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]"; @@ -37,6 +38,37 @@ export function shouldClearThreadSelectionOnMouseDown(target: HTMLElement | null return !target.closest(THREAD_SELECTION_SAFE_SELECTOR); } +export function resolveThreadRowClassName(input: { + isActive: boolean; + isSelected: boolean; +}): string { + const baseClassName = + "h-7 w-full translate-x-0 cursor-default justify-start px-2 text-left select-none focus-visible:ring-0"; + + if (input.isSelected && input.isActive) { + return cn( + baseClassName, + "bg-primary/22 text-foreground font-medium hover:bg-primary/26 hover:text-foreground dark:bg-primary/30 dark:hover:bg-primary/36", + ); + } + + if (input.isSelected) { + return cn( + baseClassName, + "bg-primary/15 text-foreground hover:bg-primary/19 hover:text-foreground dark:bg-primary/22 dark:hover:bg-primary/28", + ); + } + + if (input.isActive) { + return cn( + baseClassName, + "bg-accent/85 text-foreground font-medium hover:bg-accent hover:text-foreground dark:bg-accent/55 dark:hover:bg-accent/70", + ); + } + + return cn(baseClassName, "text-muted-foreground hover:bg-accent hover:text-foreground"); +} + export function resolveThreadStatusPill(input: { thread: ThreadStatusInput; hasPendingApprovals: boolean; diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index baf17fa996f02dd0657194b40427b019a3a6dfac..5ffd6de92f2457a699546047ee2682dfe9e5b866 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -83,7 +83,11 @@ import { import { useThreadSelectionStore } from "../threadSelectionStore"; import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; import { isNonEmpty as isNonEmptyString } from "effect/String"; -import { resolveThreadStatusPill, shouldClearThreadSelectionOnMouseDown } from "./Sidebar.logic"; +import { + resolveThreadRowClassName, + resolveThreadStatusPill, + shouldClearThreadSelectionOnMouseDown, +} from "./Sidebar.logic"; const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; const THREAD_PREVIEW_LIMIT = 6; @@ -1397,13 +1401,10 @@ export default function Sidebar() { render={
} size="sm" isActive={isActive} - className={`h-7 w-full translate-x-0 cursor-default justify-start px-2 text-left select-none hover:bg-accent hover:text-foreground focus-visible:ring-0 ${ - isSelected - ? "bg-primary/15 text-foreground dark:bg-primary/10" - : isActive - ? "bg-accent/85 text-foreground font-medium dark:bg-accent/55" - : "text-muted-foreground" - }`} + className={resolveThreadRowClassName({ + isActive, + isSelected, + })} onClick={(event) => { handleThreadClick( event, @@ -1541,7 +1542,7 @@ export default function Sidebar() { diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 7861212e4869253613c2b669f552163bd5f6609a..8e480715f54d0c3ac5a3d9167520a214217b9d2a 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -22,7 +22,7 @@ import { isTerminalClearShortcut, terminalNavigationShortcutData } from "../keyb import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID, - MAX_THREAD_TERMINAL_COUNT, + MAX_TERMINALS_PER_GROUP, type ThreadTerminalGroup, } from "../types"; import { readNativeApi } from "~/nativeApi"; @@ -605,7 +605,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 hasReachedSplitLimit = visibleTerminalIds.length >= MAX_TERMINALS_PER_GROUP; const terminalLabelById = useMemo( () => new Map( @@ -613,27 +613,24 @@ export default function ThreadTerminalDrawer({ ), [normalizedTerminalIds], ); - const splitTerminalActionLabel = hasReachedTerminalLimit - ? `Split Terminal (max ${MAX_THREAD_TERMINAL_COUNT})` + const splitTerminalActionLabel = hasReachedSplitLimit + ? `Split Terminal (max ${MAX_TERMINALS_PER_GROUP} per group)` : splitShortcutLabel ? `Split Terminal (${splitShortcutLabel})` : "Split Terminal"; - const newTerminalActionLabel = hasReachedTerminalLimit - ? `New Terminal (max ${MAX_THREAD_TERMINAL_COUNT})` - : newShortcutLabel - ? `New Terminal (${newShortcutLabel})` - : "New Terminal"; + const newTerminalActionLabel = newShortcutLabel + ? `New Terminal (${newShortcutLabel})` + : "New Terminal"; const closeTerminalActionLabel = closeShortcutLabel ? `Close Terminal (${closeShortcutLabel})` : "Close Terminal"; const onSplitTerminalAction = useCallback(() => { - if (hasReachedTerminalLimit) return; + if (hasReachedSplitLimit) return; onSplitTerminal(); - }, [hasReachedTerminalLimit, onSplitTerminal]); + }, [hasReachedSplitLimit, onSplitTerminal]); const onNewTerminalAction = useCallback(() => { - if (hasReachedTerminalLimit) return; onNewTerminal(); - }, [hasReachedTerminalLimit, onNewTerminal]); + }, [onNewTerminal]); useEffect(() => { onHeightChangeRef.current = onHeightChange; @@ -744,7 +741,7 @@ export default function ThreadTerminalDrawer({
@@ -839,7 +832,7 @@ export default function ThreadTerminalDrawer({
diff --git a/apps/web/src/hooks/useHandleNewThread.ts b/apps/web/src/hooks/useHandleNewThread.ts index e00fd6af2d3fd3c30203a19a96dce843e2fe6908..35f92d98e9b51720d07f6abaf4d69ab6f180bd94 100644 --- a/apps/web/src/hooks/useHandleNewThread.ts +++ b/apps/web/src/hooks/useHandleNewThread.ts @@ -1,32 +1,29 @@ import { DEFAULT_RUNTIME_MODE, type ProjectId, ThreadId } from "@t3tools/contracts"; import { useNavigate, useParams } from "@tanstack/react-router"; import { useCallback } from "react"; -import { type DraftThreadEnvMode, useComposerDraftStore } from "../composerDraftStore"; +import { + type DraftThreadEnvMode, + type DraftThreadState, + useComposerDraftStore, +} from "../composerDraftStore"; import { newThreadId } from "../lib/utils"; import { useStore } from "../store"; export function useHandleNewThread() { const projects = useStore((store) => store.projects); const threads = useStore((store) => store.threads); - const getDraftThreadByProjectId = useComposerDraftStore( - (store) => store.getDraftThreadByProjectId, - ); - const getDraftThread = useComposerDraftStore((store) => store.getDraftThread); - const setProjectDraftThreadId = useComposerDraftStore((store) => store.setProjectDraftThreadId); - const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); - const clearProjectDraftThreadId = useComposerDraftStore( - (store) => store.clearProjectDraftThreadId, - ); const navigate = useNavigate(); const routeThreadId = useParams({ strict: false, select: (params) => (params.threadId ? ThreadId.makeUnsafe(params.threadId) : null), }); + const activeDraftThread = useComposerDraftStore((store) => + routeThreadId ? (store.draftThreadsByThreadId[routeThreadId] ?? null) : null, + ); const activeThread = routeThreadId ? threads.find((thread) => thread.id === routeThreadId) : undefined; - const activeDraftThread = routeThreadId ? getDraftThread(routeThreadId) : null; const handleNewThread = useCallback( ( @@ -37,10 +34,20 @@ export function useHandleNewThread() { envMode?: DraftThreadEnvMode; }, ): Promise => { + const { + clearProjectDraftThreadId, + getDraftThread, + getDraftThreadByProjectId, + setDraftThreadContext, + setProjectDraftThreadId, + } = useComposerDraftStore.getState(); const hasBranchOption = options?.branch !== undefined; const hasWorktreePathOption = options?.worktreePath !== undefined; const hasEnvModeOption = options?.envMode !== undefined; const storedDraftThread = getDraftThreadByProjectId(projectId); + const latestActiveDraftThread: DraftThreadState | null = routeThreadId + ? getDraftThread(routeThreadId) + : null; if (storedDraftThread) { return (async () => { if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) { @@ -63,7 +70,11 @@ export function useHandleNewThread() { clearProjectDraftThreadId(projectId); - if (activeDraftThread && routeThreadId && activeDraftThread.projectId === projectId) { + if ( + latestActiveDraftThread && + routeThreadId && + latestActiveDraftThread.projectId === projectId + ) { if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) { setDraftThreadContext(routeThreadId, { ...(hasBranchOption ? { branch: options?.branch ?? null } : {}), @@ -92,15 +103,7 @@ export function useHandleNewThread() { }); })(); }, - [ - activeDraftThread, - clearProjectDraftThreadId, - getDraftThreadByProjectId, - navigate, - routeThreadId, - setDraftThreadContext, - setProjectDraftThreadId, - ], + [navigate, routeThreadId], ); return { diff --git a/apps/web/src/terminalStateStore.test.ts b/apps/web/src/terminalStateStore.test.ts index 1f48b1693f50ab7172e11b65734d2c2b5a2bf507..e7e240cf254069f0296dae23cce3140e581ba84f 100644 --- a/apps/web/src/terminalStateStore.test.ts +++ b/apps/web/src/terminalStateStore.test.ts @@ -46,6 +46,28 @@ describe("terminalStateStore actions", () => { ]); }); + it("caps splits at four terminals per group", () => { + const store = useTerminalStateStore.getState(); + store.splitTerminal(THREAD_ID, "terminal-2"); + store.splitTerminal(THREAD_ID, "terminal-3"); + store.splitTerminal(THREAD_ID, "terminal-4"); + store.splitTerminal(THREAD_ID, "terminal-5"); + + const terminalState = selectThreadTerminalState( + useTerminalStateStore.getState().terminalStateByThreadId, + THREAD_ID, + ); + expect(terminalState.terminalIds).toEqual([ + "default", + "terminal-2", + "terminal-3", + "terminal-4", + ]); + expect(terminalState.terminalGroups).toEqual([ + { id: "group-default", terminalIds: ["default", "terminal-2", "terminal-3", "terminal-4"] }, + ]); + }); + it("creates new terminals in a separate group", () => { useTerminalStateStore.getState().newTerminal(THREAD_ID, "terminal-2"); @@ -62,6 +84,33 @@ describe("terminalStateStore actions", () => { ]); }); + it("allows unlimited groups while keeping each group capped at four terminals", () => { + const store = useTerminalStateStore.getState(); + store.splitTerminal(THREAD_ID, "terminal-2"); + store.splitTerminal(THREAD_ID, "terminal-3"); + store.splitTerminal(THREAD_ID, "terminal-4"); + store.newTerminal(THREAD_ID, "terminal-5"); + store.newTerminal(THREAD_ID, "terminal-6"); + + const terminalState = selectThreadTerminalState( + useTerminalStateStore.getState().terminalStateByThreadId, + THREAD_ID, + ); + expect(terminalState.terminalIds).toEqual([ + "default", + "terminal-2", + "terminal-3", + "terminal-4", + "terminal-5", + "terminal-6", + ]); + expect(terminalState.terminalGroups).toEqual([ + { id: "group-default", terminalIds: ["default", "terminal-2", "terminal-3", "terminal-4"] }, + { id: "group-terminal-5", terminalIds: ["terminal-5"] }, + { id: "group-terminal-6", terminalIds: ["terminal-6"] }, + ]); + }); + it("tracks and clears terminal subprocess activity", () => { const store = useTerminalStateStore.getState(); store.splitTerminal(THREAD_ID, "terminal-2"); diff --git a/apps/web/src/terminalStateStore.ts b/apps/web/src/terminalStateStore.ts index cf1ea8446c06140567866b9eb53e4191460b3618..b2cea6d56057f4be54ca8bdb64299af3efe8eddd 100644 --- a/apps/web/src/terminalStateStore.ts +++ b/apps/web/src/terminalStateStore.ts @@ -11,7 +11,7 @@ import { createJSONStorage, persist } from "zustand/middleware"; import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID, - MAX_THREAD_TERMINAL_COUNT, + MAX_TERMINALS_PER_GROUP, type ThreadTerminalGroup, } from "./types"; @@ -28,10 +28,7 @@ interface ThreadTerminalState { const TERMINAL_STATE_STORAGE_KEY = "t3code:terminal-state:v1"; function normalizeTerminalIds(terminalIds: string[]): string[] { - const ids = [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))].slice( - 0, - MAX_THREAD_TERMINAL_COUNT, - ); + const ids = [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))]; return ids.length > 0 ? ids : [DEFAULT_THREAD_TERMINAL_ID]; } @@ -243,10 +240,6 @@ function upsertTerminalIntoGroups( } const isNewTerminal = !normalized.terminalIds.includes(terminalId); - if (isNewTerminal && normalized.terminalIds.length >= MAX_THREAD_TERMINAL_COUNT) { - return normalized; - } - const terminalIds = isNewTerminal ? [...normalized.terminalIds, terminalId] : normalized.terminalIds; @@ -297,6 +290,14 @@ function upsertTerminalIntoGroups( return normalized; } + if ( + isNewTerminal && + !destinationGroup.terminalIds.includes(terminalId) && + destinationGroup.terminalIds.length >= MAX_TERMINALS_PER_GROUP + ) { + return normalized; + } + if (!destinationGroup.terminalIds.includes(terminalId)) { const anchorIndex = destinationGroup.terminalIds.indexOf(normalized.activeTerminalId); if (anchorIndex >= 0) { diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index d5fff12991e594786aab528b6dca18e510fb3659..c071fb3f60870e8f9c6374d103501d99c11032fb 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -20,7 +20,7 @@ export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; export const DEFAULT_INTERACTION_MODE: ProviderInteractionMode = "default"; export const DEFAULT_THREAD_TERMINAL_HEIGHT = 280; export const DEFAULT_THREAD_TERMINAL_ID = "default"; -export const MAX_THREAD_TERMINAL_COUNT = 4; +export const MAX_TERMINALS_PER_GROUP = 4; export type ProjectScript = ContractProjectScript; export interface ThreadTerminalGroup {