Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,14 @@ export function CliSessionWorkSurfaceHeader({
showCacheBadge={showCache}
cacheIdleSinceAt={session.chatIdleSinceAt}
showGitToolbar
onContextMenu={
onContextMenu
? (event) => {
event.preventDefault();
onContextMenu(session, event);
}
: undefined
}
onToggleSessionsPane={onToggleSessionsPane}
sessionsPaneCollapsed={sessionsPaneCollapsed}
sessionsPaneCount={sessionsPaneCount}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type SessionContextMenuProps = {
menu: SessionContextMenuState;
onClose: () => void;
onStopRuntime: (args: { ptyId: string; sessionId: string }) => void;
onStopAndDelete: (session: TerminalSessionSummary) => void;
onDeleteChat: (session: TerminalSessionSummary) => void;
onDeleteSession: (session: TerminalSessionSummary) => void;
deletingSessionId: string | null;
Expand All @@ -30,6 +31,7 @@ export function SessionContextMenu({
menu,
onClose,
onStopRuntime,
onStopAndDelete,
onDeleteChat,
onDeleteSession,
deletingSessionId,
Expand Down Expand Up @@ -161,13 +163,23 @@ export function SessionContextMenu({
</button>
) : null}

{isRunning && session.ptyId && !isChat ? (
<button
className="flex w-full items-center gap-2 rounded px-3 py-1.5 text-left text-xs text-red-300 hover:bg-red-500/10 transition-colors"
disabled={deletingSessionId === session.id}
onClick={() => { onStopAndDelete(session); onClose(); }}
>
{deletingSessionId === session.id ? "Deleting…" : "Stop & delete"}
</button>
) : null}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

{isChat ? (
<button
className="flex w-full items-center gap-2 rounded px-3 py-1.5 text-left text-xs text-red-300 hover:bg-red-500/10 transition-colors"
disabled={deletingSessionId === session.id}
onClick={() => { onDeleteChat(session); onClose(); }}
>
{deletingSessionId === session.id ? "Deleting..." : "Delete chat"}
{deletingSessionId === session.id ? "Deleting" : "Delete chat"}
</button>
) : null}

Expand All @@ -177,7 +189,7 @@ export function SessionContextMenu({
disabled={deletingSessionId === session.id}
onClick={() => { onDeleteSession(session); onClose(); }}
>
{deletingSessionId === session.id ? "Deleting..." : "Delete session"}
{deletingSessionId === session.id ? "Deleting" : "Delete session"}
</button>
) : null}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function SessionInfoPopover({
popover,
onClose,
onStopRuntime,
onStopAndDelete,
onDeleteChat,
onDeleteSession,
onGoToLane,
Expand All @@ -88,6 +89,7 @@ export function SessionInfoPopover({
popover: InfoPopoverState;
onClose: () => void;
onStopRuntime: (args: { ptyId: string; sessionId: string }) => void;
onStopAndDelete: (session: TerminalSessionSummary) => void;
onDeleteChat: (session: TerminalSessionSummary) => void;
onDeleteSession: (session: TerminalSessionSummary) => void;
onGoToLane: (session: TerminalSessionSummary) => void;
Expand Down Expand Up @@ -290,6 +292,19 @@ export function SessionInfoPopover({
</Button>
</SmartTooltip>
) : null}
{session.status === "running" && session.ptyId && !isChat ? (
<SmartTooltip content={{ label: "Stop & delete", description: "Stop the runtime and permanently delete this session." }}>
<Button
variant="danger"
size="sm"
disabled={deletingSessionId === session.id}
onClick={() => onStopAndDelete(session)}
>
<Trash size={14} weight="regular" />
{deletingSessionId === session.id ? "Deleting…" : "Stop & delete"}
</Button>
</SmartTooltip>
) : null}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{isChat ? (
<SmartTooltip content={{ label: "Delete chat", description: "Permanently remove this chat from the project." }}>
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* @vitest-environment jsdom */

import { fireEvent, render, screen, within } from "@testing-library/react";
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
import type { ComponentProps } from "react";
import { MemoryRouter } from "react-router-dom";
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { LaneSummary, TerminalSessionSummary } from "../../../shared/types";
import { SessionListPane } from "./SessionListPane";

Expand Down Expand Up @@ -99,6 +99,10 @@ function renderPane(props: Partial<ComponentProps<typeof SessionListPane>> = {})
}

describe("SessionListPane", () => {
afterEach(() => {
cleanup();
});

it("renders by-lane sessions whose lane is missing from the cached lane list", () => {
renderPane();

Expand Down Expand Up @@ -338,4 +342,68 @@ describe("SessionListPane", () => {
expect(onBulkDelete).toHaveBeenCalledTimes(1);
expect(onClearSelection).toHaveBeenCalledTimes(1);
});

it("offers stop & delete only when the selection includes a running runtime", () => {
const onBulkStopAndDelete = vi.fn();
const running = makeSession({
id: "session-running",
laneId: "lane-known",
laneName: "Known Lane",
title: "Running shell",
toolType: "shell",
status: "running",
});
const chat = makeSession({
id: "session-chat",
laneId: "lane-known",
laneName: "Known Lane",
title: "Active chat",
toolType: "codex-chat",
status: "running",
ptyId: null,
});

renderPane({
runningFiltered: [running, chat],
selectedSessionIds: new Set([running.id, chat.id]),
sessionsGroupedByLane: new Map([[running.laneId, [running, chat]]]),
onBulkStopAndDelete,
});

// Mixed selection: the whole selection (both sessions) is targeted.
const stopAndDelete = screen.getByRole("button", { name: /stop & delete 2/i });
fireEvent.click(stopAndDelete);
expect(onBulkStopAndDelete).toHaveBeenCalledTimes(1);
});

it("hides stop & delete when nothing in the selection needs stopping", () => {
const endedShell = makeSession({
id: "session-ended",
laneId: "lane-known",
laneName: "Known Lane",
title: "Ended shell",
toolType: "shell",
status: "disposed",
runtimeState: "exited",
});
const chat = makeSession({
id: "session-chat",
laneId: "lane-known",
laneName: "Known Lane",
title: "Active chat",
toolType: "codex-chat",
status: "running",
ptyId: null,
});

renderPane({
runningFiltered: [chat],
endedFiltered: [endedShell],
selectedSessionIds: new Set([endedShell.id, chat.id]),
sessionsGroupedByLane: new Map([[chat.laneId, [chat, endedShell]]]),
onBulkStopAndDelete: vi.fn(),
});

expect(screen.queryByRole("button", { name: /stop & delete/i })).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export const SessionListPane = React.memo(function SessionListPane({
onClearSelection,
onBulkClose,
onBulkDelete,
onBulkStopAndDelete,
onContextMenu,
sessionListOrganization,
setSessionListOrganization,
Expand Down Expand Up @@ -218,6 +219,7 @@ export const SessionListPane = React.memo(function SessionListPane({
onClearSelection?: () => void;
onBulkClose?: () => void;
onBulkDelete?: () => void;
onBulkStopAndDelete?: () => void;
onContextMenu: (session: TerminalSessionSummary, e: React.MouseEvent) => void;
sessionListOrganization: WorkSessionListOrganization;
setSessionListOrganization: (v: WorkSessionListOrganization) => void;
Expand Down Expand Up @@ -735,6 +737,18 @@ export const SessionListPane = React.memo(function SessionListPane({
</button>
</SmartTooltip>
) : null}
{selectedRunningCount > 0 ? (
<SmartTooltip content={{ label: "Stop & delete selected", description: "Stop running runtimes, then permanently delete every selected session." }}>
<button
type="button"
className="inline-flex h-6 items-center gap-1 rounded-md border border-red-500/30 bg-red-500/15 px-2 text-[10px] font-medium text-red-200"
onClick={onBulkStopAndDelete}
>
<Trash size={10} />
Stop &amp; delete {selectedCount}
</button>
</SmartTooltip>
) : null}
<button
type="button"
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-fg/60 hover:bg-white/[0.06] hover:text-fg"
Expand Down
Loading
Loading