Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { useWorkspaceStoreRaw, useWorkspaceRecency } from "./stores/WorkspaceSto

import { useStableReference, compareMaps } from "./hooks/useStableReference";
import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandRegistryContext";
import { useOpenTerminal } from "./hooks/useOpenTerminal";
import type { CommandAction } from "./contexts/CommandRegistryContext";
import { useTheme, type ThemeMode } from "./contexts/ThemeContext";
import { CommandPalette } from "./components/CommandPalette";
Expand Down Expand Up @@ -201,8 +200,6 @@ function AppInner() {
}
}, [selectedWorkspace, workspaceMetadata, setSelectedWorkspace]);

const openWorkspaceInTerminal = useOpenTerminal();

const handleRemoveProject = useCallback(
async (path: string): Promise<{ success: boolean; error?: string }> => {
if (selectedWorkspace?.projectPath === path) {
Expand Down Expand Up @@ -459,12 +456,6 @@ function AppInner() {
onRemoveProject: removeProjectFromPalette,
onToggleSidebar: toggleSidebarFromPalette,
onNavigateWorkspace: navigateWorkspaceFromPalette,
onOpenWorkspaceInTerminal: (workspaceId, runtimeConfig) => {
// Best-effort only. Palette actions should never throw.
void openWorkspaceInTerminal(workspaceId, runtimeConfig).catch(() => {
// Errors are surfaced elsewhere (toasts/logs) and users can retry.
});
},
onToggleTheme: toggleTheme,
onSetTheme: setThemePreference,
onOpenSettings: openSettings,
Expand Down
4 changes: 3 additions & 1 deletion src/browser/components/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ interface ChatPaneProps {
onToggleLeftSidebarCollapsed: () => void;
runtimeConfig?: RuntimeConfig;
status?: "creating";
onOpenTerminal: (options?: TerminalSessionCreateOptions) => void;
onOpenTerminal: (
options?: TerminalSessionCreateOptions
) => Promise<{ success: boolean; error?: string }>;
}

type ReviewsState = ReturnType<typeof useReviews>;
Expand Down
65 changes: 21 additions & 44 deletions src/browser/components/RightSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,7 @@ import {
getTabName,
type TabDragData,
} from "./RightSidebar/RightSidebarTabStrip";
import {
createTerminalSession,
openTerminalPopout,
type TerminalSessionCreateOptions,
} from "@/browser/utils/terminal";
import { createTerminalSession, type TerminalSessionCreateOptions } from "@/browser/utils/terminal";
import {
CostsTabLabel,
ExplorerTabLabel,
Expand Down Expand Up @@ -167,7 +163,8 @@ interface RightSidebarProps {
isCreating?: boolean;
/** Ref callback to expose addTerminal function to parent */
addTerminalRef?: React.MutableRefObject<
((options?: TerminalSessionCreateOptions) => void) | null
| ((options?: TerminalSessionCreateOptions) => Promise<{ success: boolean; error?: string }>)
| null
>;
}

Expand Down Expand Up @@ -210,10 +207,8 @@ interface RightSidebarTabsetNodeProps {
/** Data about the currently dragged tab (if any) */
activeDragData: TabDragData | null;
setLayout: (updater: (prev: RightSidebarLayoutState) => RightSidebarLayoutState) => void;
/** Handler to pop out a terminal tab to a separate window */
onPopOutTerminal: (tab: TabType) => void;
/** Handler to add a new terminal tab */
onAddTerminal: () => void;
onAddTerminal: () => void | Promise<unknown>;
/** Handler to close a terminal tab */
onCloseTerminal: (tab: TabType) => void;
/** Map of terminal tab types to their current titles (from OSC sequences) */
Expand Down Expand Up @@ -353,7 +348,6 @@ const RightSidebarTabsetNode: React.FC<RightSidebarTabsetNodeProps> = (props) =>
<TerminalTabLabel
dynamicTitle={props.terminalTitles.get(tab)}
terminalIndex={terminalIndex}
onPopOut={() => props.onPopOutTerminal(tab)}
onClose={() => props.onCloseTerminal(tab)}
/>
);
Expand Down Expand Up @@ -953,18 +947,29 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
// Creates the backend session first, then adds the tab with the real sessionId.
// This ensures the tabType (and React key) never changes, preventing remounts.
const handleAddTerminal = React.useCallback(
(options?: TerminalSessionCreateOptions) => {
if (!api) return;
async (
options?: TerminalSessionCreateOptions
): Promise<{ success: boolean; error?: string }> => {
if (!api) {
return { success: false, error: "Not connected to server" };
}

// Also expand sidebar if collapsed
setCollapsed(false);
try {
// Also expand sidebar if collapsed
setCollapsed(false);

void createTerminalSession(api, workspaceId, options).then((session) => {
const session = await createTerminalSession(api, workspaceId, options);
const newTab = makeTerminalTabType(session.sessionId);
setLayout((prev) => addTabToFocusedTabset(prev, newTab));
// Schedule focus for this terminal (will be consumed when the tab mounts)
setAutoFocusTerminalSession(session.sessionId);
});
return { success: true };
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : "Failed to create terminal",
};
}
},
[api, workspaceId, setLayout, setCollapsed]
);
Expand Down Expand Up @@ -1005,33 +1010,6 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
[api, focusActiveTerminal, getBaseLayout, setLayout, terminalTitlesKey]
);

// Handler to pop out a terminal to a separate window, then remove the tab
const handlePopOutTerminal = React.useCallback(
(tab: TabType) => {
if (!api) return;

// Session ID is embedded in the tab type
const sessionId = getTerminalSessionId(tab);
if (!sessionId) return; // Can't pop out without a session

// Open the pop-out window (handles browser vs Electron modes)
openTerminalPopout(api, workspaceId, sessionId);

// Remove the tab from the sidebar (terminal now lives in its own window)
// Don't close the session - the pop-out window takes over
setLayout((prev) => removeTabEverywhere(prev, tab));

// Clean up title (and persist)
setTerminalTitles((prev) => {
const next = new Map(prev);
next.delete(tab);
updatePersistedState(terminalTitlesKey, Object.fromEntries(next));
return next;
});
},
[workspaceId, api, setLayout, terminalTitlesKey]
);

// Configure sensors with distance threshold for click vs drag disambiguation

// Handler to open a file in a new tab
Expand Down Expand Up @@ -1215,7 +1193,6 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
activeDragData={activeDragData}
sessionCost={sessionCost}
setLayout={setLayout}
onPopOutTerminal={handlePopOutTerminal}
onAddTerminal={handleAddTerminal}
onCloseTerminal={handleCloseTerminal}
terminalTitles={terminalTitles}
Expand Down
4 changes: 2 additions & 2 deletions src/browser/components/RightSidebar/RightSidebarTabStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ interface RightSidebarTabStripProps {
/** Unique ID of this tabset (for drag/drop) */
tabsetId: string;
/** Called when user clicks the "+" button to add a new terminal */
onAddTerminal?: () => void;
onAddTerminal?: () => void | Promise<unknown>;
}

/**
Expand Down Expand Up @@ -193,7 +193,7 @@ export const RightSidebarTabStrip: React.FC<RightSidebarTabStripProps> = ({
"text-muted hover:bg-hover hover:text-foreground shrink-0 rounded-md p-1 transition-colors",
isDesktop && "titlebar-no-drag"
)}
onClick={onAddTerminal}
onClick={() => void onAddTerminal()}
aria-label="New terminal"
>
<Plus className="h-3.5 w-3.5" />
Expand Down
23 changes: 2 additions & 21 deletions src/browser/components/RightSidebar/tabs/TabLabels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import React from "react";
import { ExternalLink, FolderTree, Terminal as TerminalIcon, X } from "lucide-react";
import { FolderTree, Terminal as TerminalIcon, X } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip";
import { FileIcon } from "../../FileIcon";
import { formatTabDuration, type ReviewStats } from "./registry";
Expand Down Expand Up @@ -114,17 +114,14 @@ interface TerminalTabLabelProps {
dynamicTitle?: string;
/** Terminal index (0-based) within the current tabset */
terminalIndex: number;
/** Callback when pop-out button is clicked */
onPopOut: () => void;
/** Callback when close button is clicked */
onClose: () => void;
}

/** Terminal tab label with icon, dynamic title, and action buttons */
/** Terminal tab label with icon, dynamic title, and close button */
export const TerminalTabLabel: React.FC<TerminalTabLabelProps> = ({
dynamicTitle,
terminalIndex,
onPopOut,
onClose,
}) => {
const fallbackName = terminalIndex === 0 ? "Terminal" : `Terminal ${terminalIndex + 1}`;
Expand All @@ -134,22 +131,6 @@ export const TerminalTabLabel: React.FC<TerminalTabLabelProps> = ({
<span className="inline-flex items-center gap-1">
<TerminalIcon className="h-3 w-3 shrink-0" />
<span className="max-w-[20ch] min-w-0 truncate">{displayName}</span>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="text-muted hover:text-foreground -my-0.5 rounded p-0.5 transition-colors"
onClick={(e) => {
e.stopPropagation();
onPopOut();
}}
aria-label="Open terminal in new window"
>
<ExternalLink className="h-3 w-3" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom">Open in new window</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
Expand Down
28 changes: 14 additions & 14 deletions src/browser/components/WorkspaceHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { Button } from "@/browser/components/ui/button";
import type { RuntimeConfig } from "@/common/types/runtime";
import { useTutorial } from "@/browser/contexts/TutorialContext";
import type { TerminalSessionCreateOptions } from "@/browser/utils/terminal";
import { useOpenTerminal } from "@/browser/hooks/useOpenTerminal";
import { useOpenInEditor } from "@/browser/hooks/useOpenInEditor";
import { usePersistedState } from "@/browser/hooks/usePersistedState";
import {
Expand All @@ -35,8 +34,10 @@ interface WorkspaceHeaderProps {
runtimeConfig?: RuntimeConfig;
leftSidebarCollapsed: boolean;
onToggleLeftSidebarCollapsed: () => void;
/** Callback to open integrated terminal in sidebar (optional, falls back to popout) */
onOpenTerminal?: (options?: TerminalSessionCreateOptions) => void;
/** Callback to open integrated terminal in sidebar */
onOpenTerminal: (
options?: TerminalSessionCreateOptions
) => Promise<{ success: boolean; error?: string }>;
}

export const WorkspaceHeader: React.FC<WorkspaceHeaderProps> = ({
Expand All @@ -50,13 +51,13 @@ export const WorkspaceHeader: React.FC<WorkspaceHeaderProps> = ({
onToggleLeftSidebarCollapsed,
onOpenTerminal,
}) => {
const openTerminalPopout = useOpenTerminal();
const openInEditor = useOpenInEditor();
const gitStatus = useGitStatus(workspaceId);
const { canInterrupt, isStarting, awaitingUserQuestion } = useWorkspaceSidebarState(workspaceId);
const isWorking = (canInterrupt || isStarting) && !awaitingUserQuestion;
const { startSequence: startTutorial } = useTutorial();
const [editorError, setEditorError] = useState<string | null>(null);
const [terminalError, setTerminalError] = useState<string | null>(null);
const [debugLlmRequestOpen, setDebugLlmRequestOpen] = useState(false);
const [mcpModalOpen, setMcpModalOpen] = useState(false);

Expand All @@ -65,16 +66,14 @@ export const WorkspaceHeader: React.FC<WorkspaceHeaderProps> = ({
listener: true,
});

const handleOpenTerminal = useCallback(() => {
// On mobile touch devices, always use popout since the right sidebar is hidden
const isMobileTouch = window.matchMedia("(max-width: 768px) and (pointer: coarse)").matches;
if (onOpenTerminal && !isMobileTouch) {
onOpenTerminal();
} else {
// Fallback to popout if no integrated terminal callback provided or on mobile
void openTerminalPopout(workspaceId, runtimeConfig);
const handleOpenTerminal = useCallback(async () => {
setTerminalError(null);
const result = await onOpenTerminal();
if (!result.success && result.error) {
setTerminalError(result.error);
setTimeout(() => setTerminalError(null), 3000);
}
}, [workspaceId, openTerminalPopout, runtimeConfig, onOpenTerminal]);
}, [onOpenTerminal]);

const handleOpenInEditor = useCallback(async () => {
setEditorError(null);
Expand Down Expand Up @@ -163,6 +162,7 @@ export const WorkspaceHeader: React.FC<WorkspaceHeaderProps> = ({
<div className={cn("flex items-center gap-2", isDesktop && "titlebar-no-drag")}>
<WorkspaceLinks workspaceId={workspaceId} />
{editorError && <span className="text-danger-soft text-xs">{editorError}</span>}
{terminalError && <span className="text-danger-soft text-xs">{terminalError}</span>}
<Tooltip>
<TooltipTrigger asChild>
<Button
Expand Down Expand Up @@ -199,7 +199,7 @@ export const WorkspaceHeader: React.FC<WorkspaceHeaderProps> = ({
<Button
variant="ghost"
size="icon"
onClick={handleOpenTerminal}
onClick={() => void handleOpenTerminal()}
className="text-muted hover:text-foreground ml-1 h-6 w-6 shrink-0 [&_svg]:h-4 [&_svg]:w-4"
data-tutorial="terminal-button"
>
Expand Down
22 changes: 11 additions & 11 deletions src/browser/components/WorkspaceShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import React, { useCallback, useRef } from "react";
import { cn } from "@/common/lib/utils";
import { RIGHT_SIDEBAR_WIDTH_KEY } from "@/common/constants/storage";
import { useResizableSidebar } from "@/browser/hooks/useResizableSidebar";
import { useOpenTerminal } from "@/browser/hooks/useOpenTerminal";
import { RightSidebar } from "./RightSidebar";
import { PopoverError } from "./PopoverError";
import type { RuntimeConfig } from "@/common/types/runtime";
Expand Down Expand Up @@ -56,19 +55,20 @@ export const WorkspaceShell: React.FC<WorkspaceShellProps> = (props) => {
});

const { width: sidebarWidth, isResizing, startResize } = sidebar;
const addTerminalRef = useRef<((options?: TerminalSessionCreateOptions) => void) | null>(null);
const openTerminalPopout = useOpenTerminal();
const addTerminalRef = useRef<
| ((options?: TerminalSessionCreateOptions) => Promise<{ success: boolean; error?: string }>)
| null
>(null);
const handleOpenTerminal = useCallback(
(options?: TerminalSessionCreateOptions) => {
// On mobile touch devices, always use popout since the right sidebar is hidden
const isMobileTouch = window.matchMedia("(max-width: 768px) and (pointer: coarse)").matches;
if (isMobileTouch) {
void openTerminalPopout(props.workspaceId, props.runtimeConfig, options);
} else {
addTerminalRef.current?.(options);
async (
options?: TerminalSessionCreateOptions
): Promise<{ success: boolean; error?: string }> => {
if (!addTerminalRef.current) {
return { success: false, error: "Terminal not available" };
}
return addTerminalRef.current(options);
},
[openTerminalPopout, props.workspaceId, props.runtimeConfig]
[]
);

const reviews = useReviews(props.workspaceId);
Expand Down
5 changes: 0 additions & 5 deletions src/browser/contexts/WorkspaceContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1059,16 +1059,11 @@ function createMockAPI(options: MockAPIOptions = {}) {
getLaunchProject: mock(options.server?.getLaunchProject ?? (() => Promise.resolve(null))),
};

const terminal = {
openWindow: mock(() => Promise.resolve()),
};

// Update the global mock
currentClientMock = {
workspace,
projects,
server,
terminal,
};

return { workspace, projects, window: happyWindow };
Expand Down
4 changes: 2 additions & 2 deletions src/browser/hooks/useAIViewKeybinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface UseAIViewKeybindsParams {
showRetryBarrier: boolean;
chatInputAPI: React.RefObject<ChatInputAPI | null>;
jumpToBottom: () => void;
handleOpenTerminal: () => void;
handleOpenTerminal: () => void | Promise<unknown>;
handleOpenInEditor: () => void;
aggregator: StreamingMessageAggregator | undefined; // For compaction detection
setEditingMessage: (editing: { id: string; content: string } | undefined) => void;
Expand Down Expand Up @@ -109,7 +109,7 @@ export function useAIViewKeybinds({
}
if (matchesKeybind(e, KEYBINDS.OPEN_TERMINAL)) {
e.preventDefault();
handleOpenTerminal();
void handleOpenTerminal();
return;
}

Expand Down
Loading
Loading