Skip to content
Closed
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
@@ -0,0 +1,52 @@
// @vitest-environment jsdom

import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { TerminalLinkOpenDialog } from "./TerminalLinkOpenDialog";

const TARGET = {
source: "osc8" as const,
uri: "https://example.com/hidden-target?token=visible",
};

afterEach(cleanup);

describe("TerminalLinkOpenDialog", () => {
it("discloses the exact target and cancels without opening it", () => {
const onConfirm = vi.fn();
const onOpenChange = vi.fn();

render(
<TerminalLinkOpenDialog
target={TARGET}
onConfirm={onConfirm}
onOpenChange={onOpenChange}
/>,
);

expect(screen.getByRole("dialog")).toBeTruthy();
expect(screen.getByLabelText("Link target").textContent).toBe(TARGET.uri);

fireEvent.click(screen.getByRole("button", { name: "Cancel" }));

expect(onOpenChange).toHaveBeenCalledWith(false);
expect(onConfirm).not.toHaveBeenCalled();
});

it("confirms the exact disclosed target", () => {
const onConfirm = vi.fn();

render(
<TerminalLinkOpenDialog
target={TARGET}
onConfirm={onConfirm}
onOpenChange={vi.fn()}
/>,
);

fireEvent.click(screen.getByRole("button", { name: "Open" }));

expect(onConfirm).toHaveBeenCalledOnce();
expect(onConfirm).toHaveBeenCalledWith(TARGET);
});
});
58 changes: 58 additions & 0 deletions apps/app/src/components/thread/terminal/TerminalLinkOpenDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Button } from "@bb/shared-ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@bb/shared-ui/dialog";
import type { TerminalLinkTarget } from "./terminal-links";

interface TerminalLinkOpenDialogProps {
onConfirm: (target: TerminalLinkTarget) => void;
onOpenChange: (open: boolean) => void;
target: TerminalLinkTarget | null;
}

export function TerminalLinkOpenDialog({
onConfirm,
onOpenChange,
target,
}: TerminalLinkOpenDialogProps) {
return (
<Dialog open={target !== null} onOpenChange={onOpenChange}>
<DialogContent>
{target ? (
<>
<DialogHeader>
<DialogTitle>Open terminal link?</DialogTitle>
<DialogDescription>
Terminal output can disguise a link destination. Check the
address before opening it.
</DialogDescription>
</DialogHeader>
<div
aria-label="Link target"
className="max-h-40 overflow-y-auto break-all rounded-md border bg-muted/40 p-3 font-mono text-xs text-foreground"
>
{target.uri}
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
>
Cancel
</Button>
<Button type="button" onClick={() => onConfirm(target)}>
Open
</Button>
</DialogFooter>
</>
) : null}
</DialogContent>
</Dialog>
);
}
101 changes: 101 additions & 0 deletions apps/app/src/components/thread/terminal/ThreadTerminalView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Terminal } from "@xterm/xterm";
import { describe, expect, it, vi } from "vitest";
import {
buildTerminalThemeFromCssColors,
captureTerminalContextMenuState,
decodeTerminalOutputBytes,
encodeTerminalInputChunks,
focusTerminalFromTouchRelease,
Expand All @@ -17,6 +18,106 @@ import {
writeTerminalOutput,
updateTerminalTouchFocusGesture,
} from "./ThreadTerminalView";
import {
createTerminalOsc8LinkHandler,
requestTerminalLinkOpen,
} from "./terminal-links";

describe("terminal hyperlinks", () => {
it("preserves OSC-8 provenance through hover and primary activation", () => {
const onActivate = vi.fn();
const onHover = vi.fn();
const handler = createTerminalOsc8LinkHandler({
onActivate,
onHover,
});
const event = { button: 0 } as MouseEvent;
const range = {
start: { x: 1, y: 1 },
end: { x: 1, y: 1 },
};

handler.hover?.(event, "https://example.com/authorize", range);
handler.activate(event, "https://example.com/authorize", range);
handler.leave?.(event, "https://example.com/authorize", range);

expect(onActivate).toHaveBeenCalledWith({
source: "osc8",
uri: "https://example.com/authorize",
});
expect(onHover).toHaveBeenNthCalledWith(1, {
source: "osc8",
uri: "https://example.com/authorize",
});
expect(onHover).toHaveBeenNthCalledWith(2, null);
});

it("does not activate OSC-8 links from a secondary click", () => {
const onActivate = vi.fn();
const handler = createTerminalOsc8LinkHandler({
onActivate,
onHover: vi.fn(),
});
const range = {
start: { x: 1, y: 1 },
end: { x: 1, y: 1 },
};

handler.activate(
{ button: 2 } as MouseEvent,
"https://example.com/right-click",
range,
);

expect(onActivate).not.toHaveBeenCalled();
});

it("confirms concealed targets and directly opens detected URLs", () => {
const openLink = vi.fn();
const requestConfirmation = vi.fn();

requestTerminalLinkOpen({
openLink,
requestConfirmation,
target: { source: "osc8", uri: "https://example.com/concealed" },
});
requestTerminalLinkOpen({
openLink,
requestConfirmation,
target: {
source: "detected-url",
uri: "https://example.com/visible",
},
});

expect(requestConfirmation).toHaveBeenCalledOnce();
expect(requestConfirmation).toHaveBeenCalledWith({
source: "osc8",
uri: "https://example.com/concealed",
});
expect(openLink).toHaveBeenCalledOnce();
expect(openLink).toHaveBeenCalledWith("https://example.com/visible");
});

it("preserves link actions while copying the exact xterm selection", () => {
const getSelection = vi.fn(() => " wrapped terminal selection\n");
const link = {
source: "detected-url" as const,
uri: "https://example.com/visible",
};

expect(
captureTerminalContextMenuState({
link,
terminal: { getSelection },
}),
).toEqual({
link,
selectionText: " wrapped terminal selection\n",
});
expect(getSelection).toHaveBeenCalledOnce();
});
});

function startTouchFocusGesture() {
const gesture = startTerminalTouchFocusGesture(
Expand Down
Loading