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
30 changes: 5 additions & 25 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3014,29 +3014,6 @@ function Workspace({
onOpenTerminal(terminalCwd);
}, [terminalCwd, onOpenTerminal]);

const onShowProjectTerminal = useCallback(() => {
leaveCoveringMono();
const dock = findProjectTerminal(projectTerminalsRef.current, projectCwd);
if (dock && dock.pane.files.length > 0) {
if (!dock.open) {
setProjectTerminals((prev) =>
mapProjectTerminal(prev, projectCwd, (entry) =>
withDockOpen(entry, true),
),
);
}
focusProjectTerminal();
return;
}
onOpenTerminal(terminalCwd);
}, [
terminalCwd,
focusProjectTerminal,
leaveCoveringMono,
onOpenTerminal,
projectCwd,
]);

const onNewTerminalInSession = useCallback(
(sessionId: string) => {
const session = sessionsRef.current.find(
Expand Down Expand Up @@ -13381,16 +13358,19 @@ function Workspace({
? onNewTerminal
: undefined
}
onShowTerminal={
onToggleProjectTerminal={
!monoCovers && isLocalProject(projectCwd)
? onShowProjectTerminal
? onToggleProjectTerminal
: undefined
}
projectTerminalActive={
!monoCovers &&
!!currentProjectDock &&
currentProjectDock.pane.files.length > 0
}
projectTerminalOpen={
!monoCovers && !!currentProjectDock?.open
}
/>
)}
</div>
Expand Down
120 changes: 117 additions & 3 deletions src/app/shell/UsageFooter.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
import { createElement } from "react";
// @vitest-environment happy-dom
import { act, createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { UsageFooter } from "./UsageFooter";

let container: HTMLDivElement;
let root: Root | null = null;

beforeEach(() => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
container = document.createElement("div");
document.body.append(container);
});

afterEach(() => {
if (root) {
act(() => root!.unmount());
root = null;
}
container.remove();
vi.unstubAllGlobals();
});

describe("UsageFooter terminal control", () => {
it("replaces the generic terminal button with the live process control", () => {
const markup = renderToStaticMarkup(
Expand All @@ -19,7 +39,7 @@ describe("UsageFooter terminal control", () => {
terminalOpen: true,
onToggleTerminal: vi.fn(),
onNewTerminal: vi.fn(),
onShowTerminal: vi.fn(),
onToggleProjectTerminal: vi.fn(),
projectTerminalActive: true,
}),
);
Expand All @@ -40,4 +60,98 @@ describe("UsageFooter terminal control", () => {
expect(markup).toContain(">Terminal</span>");
expect(markup.match(/<button/g)).toHaveLength(1);
});

it("marks the terminal button pressed only while the dock is open", () => {
const open = renderToStaticMarkup(
createElement(UsageFooter, {
providers: [],
onNewTerminal: vi.fn(),
onToggleProjectTerminal: vi.fn(),
projectTerminalActive: true,
projectTerminalOpen: true,
}),
);
const collapsed = renderToStaticMarkup(
createElement(UsageFooter, {
providers: [],
onNewTerminal: vi.fn(),
onToggleProjectTerminal: vi.fn(),
projectTerminalActive: true,
projectTerminalOpen: false,
}),
);

// Label still reflects "has terminals" in both states.
expect(open).toContain('aria-label="Terminal"');
expect(collapsed).toContain('aria-label="Terminal"');
expect(open).toContain('aria-pressed="true"');
expect(collapsed).toContain('aria-pressed="false"');
});

it("toggles the dock instead of only showing it", () => {
const onToggleProjectTerminal = vi.fn();
const onNewTerminal = vi.fn();
root = createRoot(container);
act(() =>
root!.render(
createElement(UsageFooter, {
providers: [],
onNewTerminal,
onToggleProjectTerminal,
projectTerminalActive: true,
projectTerminalOpen: true,
}),
),
);

const button = container.querySelector('button[aria-label="Terminal"]');
expect(button).not.toBeNull();
act(() => (button as HTMLButtonElement).click());
expect(onToggleProjectTerminal).toHaveBeenCalledTimes(1);
expect(onNewTerminal).not.toHaveBeenCalled();
});

it.each([false, true])(
"handles a toggle-only caller when projectTerminalActive is %s",
(projectTerminalActive) => {
const onToggleProjectTerminal = vi.fn();
root = createRoot(container);
act(() =>
root!.render(
createElement(UsageFooter, {
providers: [],
onToggleProjectTerminal,
projectTerminalActive,
}),
),
);

const button = container.querySelector("button");
expect(button).not.toBeNull();
act(() => button!.click());
expect(onToggleProjectTerminal).toHaveBeenCalledTimes(1);
},
);

it("prefers creating a terminal when no project terminal is active", () => {
const onNewTerminal = vi.fn();
const onToggleProjectTerminal = vi.fn();
root = createRoot(container);
act(() =>
root!.render(
createElement(UsageFooter, {
providers: [],
onNewTerminal,
onToggleProjectTerminal,
projectTerminalActive: false,
}),
),
);

const button = container.querySelector("button");
expect(button).not.toBeNull();
act(() => button!.click());
expect(onNewTerminal).toHaveBeenCalledTimes(1);
expect(onToggleProjectTerminal).not.toHaveBeenCalled();
});
});
18 changes: 11 additions & 7 deletions src/app/shell/UsageFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ export function UsageFooter({
terminalOpen = false,
onToggleTerminal,
onNewTerminal,
onShowTerminal,
onToggleProjectTerminal,
projectTerminalActive = false,
projectTerminalOpen = false,
onSelectAccount,
onManageAccounts,
}: {
Expand All @@ -85,8 +86,9 @@ export function UsageFooter({
terminalOpen?: boolean;
onToggleTerminal?: (fileId: string) => void;
onNewTerminal?: () => void;
onShowTerminal?: () => void;
onToggleProjectTerminal?: () => void;
projectTerminalActive?: boolean;
projectTerminalOpen?: boolean;
onSelectAccount?: (
provider: ProviderAccountProvider,
accountId: string,
Expand All @@ -98,13 +100,15 @@ export function UsageFooter({
providers.some((provider) => provider !== "opencode") ||
(providers.includes("opencode") && opencode.status !== "unavailable");
const showTerminals = terminals.length > 0;
const showTerminalButton = Boolean(onNewTerminal || onShowTerminal);
const showTerminalButton = Boolean(
onNewTerminal || onToggleProjectTerminal,
);
const terminalLabel = projectTerminalActive
? "Terminal"
: `New Terminal (${MOD}\`)`;
const onTerminalClick = projectTerminalActive
? (onShowTerminal ?? onNewTerminal)
: (onNewTerminal ?? onShowTerminal);
? (onToggleProjectTerminal ?? onNewTerminal)
: (onNewTerminal ?? onToggleProjectTerminal);
const ariaLabel = showUsage || session?.harness === "pi"
? "Provider usage"
: showTerminals || showTerminalButton
Expand Down Expand Up @@ -145,12 +149,12 @@ export function UsageFooter({
<button
type="button"
className={`inline-flex h-5 shrink-0 items-center gap-1.5 whitespace-nowrap rounded px-1.5 hover:bg-content/10 ${
projectTerminalActive
projectTerminalOpen
? "text-accent"
: "text-content/40 hover:text-content"
}`}
aria-label={terminalLabel}
aria-pressed={projectTerminalActive}
aria-pressed={projectTerminalOpen}
title={terminalLabel}
onClick={onTerminalClick}
>
Expand Down
Loading