mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
fix(site/src): horizontally scroll wide code blocks in agent chat (#26016)
closes CODAGT-467 ## Problem In the agent chat timeline, wide tool-preview code blocks did not wrap **and** had no usable horizontal scroll. Long lines were clipped and unreachable for mouse users. Root cause: file/code/JSON previews (`read_file`, generic & MCP tool input/output) and markdown fenced code blocks render through `@pierre/diffs`, whose `[data-code]` grid grows to its content width (`align-self: flex-start`). The wrapping `ScrollArea` only rendered a **vertical** scrollbar, so although the viewport was horizontally scrollable, there was no scrollbar affordance and the overflow was clipped. ## Fix Render a horizontal scrollbar on these previews via `ScrollArea orientation="both"`, exposing the already-scrollable viewport with a visible 6px bar (consistent with the existing hover scrollbars). For markdown, let `[data-code]` size to its content so the outer `ScrollArea` owns the scroll. Shell/log output (`execute`, `process_output`) and diffs (`write_file`, `edit_files`) intentionally keep **wrapping**. | Preview type | Behavior | | --- | --- | | `read_file`, generic/MCP input & output, markdown fenced code | horizontal **scroll** (new) | | `execute`, `process_output` (shell/logs) | wrap (unchanged) | | `write_file`, `edit_files` (diffs) | wrap (unchanged) | ## Changes - `ScrollArea`: add `horizontalScrollBarClassName` to size the horizontal bar independently of the vertical bar (avoids a `twMerge` width/height conflict). - `ReadFileTool`, generic `ToolFileViewer` (`Tool.tsx`): `orientation="both"` + thin horizontal bar. - `Response.tsx`: wrap fenced code in a both-axis `ScrollArea`; let `[data-code]` size to content. - Long-line regression stories for the file viewer, generic tool, and markdown. - Change scrollbar color to accessible contrast ratio - Increase hit area for scroll bars to 24px which is minimim for wcag 2.2 accessibility requirements <details> <summary>Implementation notes & decisions</summary> - The `@pierre/diffs` `File` viewer only supports `overflow: "scroll" | "wrap"`. Its `[data-code]` element is a grid with `overflow: scroll clip` that grows to content width instead of scrolling, so the outer container must provide the scroll affordance. - Chosen approach: surface the **outer** `ScrollArea`'s horizontal scrollbar (the viewport was already scrollable) rather than fighting the library's internal per-block scroll. This yields a single, unified horizontal scrollbar and guarantees the timeline never exceeds the viewport (the `ScrollArea` root is `overflow: hidden`). - Scroll vs wrap was chosen per content type: code/JSON/file structure benefits from scrolling (wrapping breaks indentation and line-number alignment), while shell/log output keeps wrapping. Precedent for visible horizontal scrollbars already exists in `GitPanel`/`TaskApps`. </details> --- _Generated by Coder Agents on behalf of @jaaydenh._
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, waitFor } from "storybook/test";
|
||||
import { ScrollArea } from "./ScrollArea";
|
||||
|
||||
const meta: Meta<typeof ScrollArea> = {
|
||||
title: "components/ScrollArea",
|
||||
component: ScrollArea,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ScrollArea>;
|
||||
|
||||
const OverflowingContent = () => (
|
||||
<div className="w-[1200px] p-3 font-mono text-xs leading-5">
|
||||
{Array.from({ length: 60 }, (_, row) => (
|
||||
<div key={row} className="whitespace-nowrap">
|
||||
{`row ${row.toString().padStart(2, "0")} `}
|
||||
{"value ".repeat(30)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const luminance = (color: string): number => {
|
||||
const parts = (color.match(/[\d.]+/g) ?? []).map(Number);
|
||||
const [r, g, b] = parts.slice(0, 3).map((value) => {
|
||||
const channel = value / 255;
|
||||
return channel <= 0.03928
|
||||
? channel / 12.92
|
||||
: ((channel + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
};
|
||||
|
||||
const contrastRatio = (a: string, b: string): number => {
|
||||
const la = luminance(a);
|
||||
const lb = luminance(b);
|
||||
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
||||
};
|
||||
|
||||
export const Accessibility: Story = {
|
||||
render: () => (
|
||||
<div
|
||||
data-testid="surface"
|
||||
className="w-96 rounded-md border border-solid border-border-default bg-surface-primary"
|
||||
>
|
||||
<ScrollArea
|
||||
className="h-48"
|
||||
type="always"
|
||||
orientation="both"
|
||||
scrollBarClassName="w-1.5"
|
||||
horizontalScrollBarClassName="h-1.5"
|
||||
>
|
||||
<OverflowingContent />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const surface = canvasElement.querySelector<HTMLElement>(
|
||||
"[data-testid='surface']",
|
||||
);
|
||||
await expect(surface).not.toBeNull();
|
||||
|
||||
const getThumbs = () => {
|
||||
const vertical = canvasElement.querySelector(
|
||||
'[data-orientation="vertical"]',
|
||||
)?.firstElementChild as HTMLElement | null | undefined;
|
||||
const horizontal = canvasElement.querySelector(
|
||||
'[data-orientation="horizontal"]',
|
||||
)?.firstElementChild as HTMLElement | null | undefined;
|
||||
return { vertical, horizontal };
|
||||
};
|
||||
|
||||
await waitFor(() => {
|
||||
const { vertical, horizontal } = getThumbs();
|
||||
expect(vertical).toBeTruthy();
|
||||
expect(horizontal).toBeTruthy();
|
||||
});
|
||||
|
||||
const { vertical, horizontal } = getThumbs();
|
||||
if (!vertical || !horizontal || !surface) {
|
||||
throw new Error("scrollbar thumbs not found");
|
||||
}
|
||||
|
||||
const verticalBefore = getComputedStyle(vertical, "::before");
|
||||
await expect(
|
||||
Number.parseFloat(verticalBefore.width),
|
||||
).toBeGreaterThanOrEqual(24);
|
||||
await expect(
|
||||
Number.parseFloat(verticalBefore.height),
|
||||
).toBeGreaterThanOrEqual(24);
|
||||
|
||||
const horizontalBefore = getComputedStyle(horizontal, "::before");
|
||||
await expect(
|
||||
Number.parseFloat(horizontalBefore.width),
|
||||
).toBeGreaterThanOrEqual(24);
|
||||
await expect(
|
||||
Number.parseFloat(horizontalBefore.height),
|
||||
).toBeGreaterThanOrEqual(24);
|
||||
|
||||
const thumbColor = getComputedStyle(vertical).backgroundColor;
|
||||
const surfaceColor = getComputedStyle(surface).backgroundColor;
|
||||
await expect(
|
||||
contrastRatio(thumbColor, surfaceColor),
|
||||
).toBeGreaterThanOrEqual(3);
|
||||
},
|
||||
};
|
||||
@@ -3,12 +3,13 @@
|
||||
* @see {@link https://ui.shadcn.com/docs/components/scroll-area}
|
||||
*/
|
||||
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
interface ScrollAreaProps
|
||||
extends React.ComponentPropsWithRef<typeof ScrollAreaPrimitive.Root> {
|
||||
scrollBarClassName?: string;
|
||||
horizontalScrollBarClassName?: string;
|
||||
viewportClassName?: string;
|
||||
viewportTabIndex?: number;
|
||||
/** Which scrollbar(s) to show. Defaults to "vertical". */
|
||||
@@ -18,6 +19,7 @@ interface ScrollAreaProps
|
||||
export const ScrollArea: React.FC<ScrollAreaProps> = ({
|
||||
className,
|
||||
scrollBarClassName,
|
||||
horizontalScrollBarClassName,
|
||||
viewportClassName,
|
||||
viewportTabIndex,
|
||||
orientation = "vertical",
|
||||
@@ -26,21 +28,22 @@ export const ScrollArea: React.FC<ScrollAreaProps> = ({
|
||||
}) => {
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Translate vertical wheel events into horizontal scroll when the
|
||||
// scroll area only scrolls horizontally. Without this, the mouse
|
||||
// wheel does nothing on a horizontal-only container.
|
||||
const handleWheel = useCallback(
|
||||
(e: React.WheelEvent<HTMLDivElement>) => {
|
||||
if (orientation !== "horizontal") return;
|
||||
const el = viewportRef.current;
|
||||
if (!el) return;
|
||||
// Only redirect when the user is scrolling vertically.
|
||||
useEffect(() => {
|
||||
const el = viewportRef.current;
|
||||
if (!el || orientation === "vertical") return;
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
||||
if (el.scrollWidth <= el.clientWidth) return;
|
||||
if (el.scrollHeight > el.clientHeight) return;
|
||||
const maxLeft = el.scrollWidth - el.clientWidth;
|
||||
if (e.deltaY > 0 && el.scrollLeft >= maxLeft) return;
|
||||
if (e.deltaY < 0 && el.scrollLeft <= 0) return;
|
||||
e.preventDefault();
|
||||
el.scrollBy({ left: e.deltaY, behavior: "smooth" });
|
||||
},
|
||||
[orientation],
|
||||
);
|
||||
};
|
||||
el.addEventListener("wheel", handleWheel, { passive: false });
|
||||
return () => el.removeEventListener("wheel", handleWheel);
|
||||
}, [orientation]);
|
||||
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
@@ -50,7 +53,6 @@ export const ScrollArea: React.FC<ScrollAreaProps> = ({
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
ref={viewportRef}
|
||||
tabIndex={viewportTabIndex}
|
||||
onWheel={handleWheel}
|
||||
className={cn("h-full w-full rounded-[inherit]", viewportClassName)}
|
||||
>
|
||||
{children}
|
||||
@@ -64,7 +66,12 @@ export const ScrollArea: React.FC<ScrollAreaProps> = ({
|
||||
{(orientation === "horizontal" || orientation === "both") && (
|
||||
<ScrollBar
|
||||
orientation="horizontal"
|
||||
className={cn("z-10", scrollBarClassName)}
|
||||
className={cn(
|
||||
"z-10",
|
||||
orientation === "both"
|
||||
? horizontalScrollBarClassName
|
||||
: (horizontalScrollBarClassName ?? scrollBarClassName),
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
@@ -88,7 +95,15 @@ export const ScrollBar: React.FC<
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-surface-quaternary" />
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
className={cn(
|
||||
"relative flex-1 rounded-full bg-surface-invert-secondary",
|
||||
"before:absolute before:content-['']",
|
||||
orientation === "vertical"
|
||||
? "before:right-0 before:top-1/2 before:h-full before:min-h-6 before:w-6 before:-translate-y-1/2"
|
||||
: "before:bottom-0 before:left-1/2 before:w-full before:min-w-6 before:h-6 before:-translate-x-1/2",
|
||||
)}
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -144,6 +144,7 @@ const expectCodeBlock = async (
|
||||
expect(codeStyles.paddingTop).toBe("8px");
|
||||
expect(codeStyles.paddingBottom).toBe("8px");
|
||||
expect(codeStyles.paddingBottom).toBe(codeStyles.paddingTop);
|
||||
expect(codeStyles.overflow).toBe("visible");
|
||||
|
||||
const lineStyles = getComputedStyle(line);
|
||||
expect(lineStyles.paddingLeft).toBe("12px");
|
||||
@@ -184,6 +185,67 @@ export const SingleLineFencedBlock: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
const longLineCodeBlockMarkdown = [
|
||||
"```ts",
|
||||
'const config = { apiUrl: "https://coder.example.com/api/v2/workspaces", token: "abcdefghijklmnopqrstuvwxyz0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", retries: 5 };',
|
||||
"```",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
export const LongLineFencedBlock: Story = {
|
||||
args: {
|
||||
children: longLineCodeBlockMarkdown,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await expectCodeBlock(canvasElement, /apiUrl/);
|
||||
const viewport = [
|
||||
...canvasElement.querySelectorAll<HTMLElement>(
|
||||
"[data-radix-scroll-area-viewport]",
|
||||
),
|
||||
].find((v) => v.scrollWidth > v.clientWidth);
|
||||
if (!viewport) {
|
||||
throw new Error("Expected a horizontally scrollable viewport.");
|
||||
}
|
||||
viewport.dispatchEvent(
|
||||
new WheelEvent("wheel", { deltaY: 200, bubbles: true, cancelable: true }),
|
||||
);
|
||||
await waitFor(() => expect(viewport.scrollLeft).toBeGreaterThan(0));
|
||||
},
|
||||
};
|
||||
|
||||
export const LongLineFencedBlockWheelEdges: Story = {
|
||||
args: {
|
||||
children: longLineCodeBlockMarkdown,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await expectCodeBlock(canvasElement, /apiUrl/);
|
||||
const viewport = [
|
||||
...canvasElement.querySelectorAll<HTMLElement>(
|
||||
"[data-radix-scroll-area-viewport]",
|
||||
),
|
||||
].find((v) => v.scrollWidth > v.clientWidth);
|
||||
if (!viewport) {
|
||||
throw new Error("Expected a horizontally scrollable viewport.");
|
||||
}
|
||||
const dispatchWheel = (deltaY: number) => {
|
||||
const event = new WheelEvent("wheel", {
|
||||
deltaY,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
viewport.dispatchEvent(event);
|
||||
return event.defaultPrevented;
|
||||
};
|
||||
const maxLeft = viewport.scrollWidth - viewport.clientWidth;
|
||||
viewport.scrollLeft = Math.floor(maxLeft / 2);
|
||||
expect(dispatchWheel(200)).toBe(true);
|
||||
viewport.scrollLeft = maxLeft;
|
||||
expect(dispatchWheel(200)).toBe(false);
|
||||
viewport.scrollLeft = 0;
|
||||
expect(dispatchWheel(-200)).toBe(false);
|
||||
},
|
||||
};
|
||||
|
||||
export const MarkdownAndLinksLight: Story = {
|
||||
globals: {
|
||||
theme: "light",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Streamdown,
|
||||
type UrlTransform,
|
||||
} from "streamdown";
|
||||
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
interface ResponseProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
|
||||
@@ -86,8 +87,7 @@ type FileViewerThemeType = keyof typeof fileViewerTheme;
|
||||
const markdownFileViewerCSS = [
|
||||
":host { background-color: transparent !important; }",
|
||||
"pre, [data-code], [data-line], [data-diffs-header] { background-color: transparent !important; }",
|
||||
"[data-code] { padding-block: 8px !important; overflow: auto clip !important; scrollbar-width: none !important; }",
|
||||
"[data-code]::-webkit-scrollbar { width: 0 !important; height: 0 !important; }",
|
||||
"[data-code] { padding-block: 8px !important; overflow: visible !important; }",
|
||||
"[data-disable-line-numbers][data-file] { --diffs-grid-number-column-width: 0px !important; }",
|
||||
"[data-disable-line-numbers] [data-column-number] { min-width: 0 !important; padding: 0 !important; }",
|
||||
"[data-line] { min-height: 20px !important; padding-inline: 12px !important; }",
|
||||
@@ -216,7 +216,12 @@ const createComponents = (
|
||||
const content = getHastText(codeChild).trimEnd();
|
||||
if (content) {
|
||||
return (
|
||||
<div className="my-4 overflow-hidden rounded-md border border-solid border-border-default bg-surface-primary">
|
||||
<ScrollArea
|
||||
orientation="both"
|
||||
className="my-4 rounded-md border border-solid border-border-default bg-surface-primary"
|
||||
scrollBarClassName="w-1.5"
|
||||
horizontalScrollBarClassName="h-1.5"
|
||||
>
|
||||
<FileViewer
|
||||
file={{
|
||||
name: `block.${lang}`,
|
||||
@@ -234,7 +239,7 @@ const createComponents = (
|
||||
}}
|
||||
style={markdownFileViewerStyle}
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,3 +214,33 @@ export const ParsedCommandsWithIntent: Story = {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const LongUnbrokenLineOutput: Story = {
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-72">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
args: {
|
||||
command: "cat access-token.txt",
|
||||
transcriptBlocks: [
|
||||
{
|
||||
kind: "output",
|
||||
text: `token:${"A".repeat(400)}:end`,
|
||||
},
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const viewport = canvasElement.querySelector<HTMLElement>(
|
||||
"[data-radix-scroll-area-viewport]",
|
||||
);
|
||||
await expect(viewport).not.toBeNull();
|
||||
if (viewport) {
|
||||
await expect(viewport.scrollWidth).toBeLessThanOrEqual(
|
||||
viewport.clientWidth + 2,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -229,7 +229,7 @@ const ShellTranscriptBody: React.FC<{
|
||||
scrollBarClassName="w-1.5"
|
||||
>
|
||||
<div className="px-3 py-2.5">
|
||||
<pre className="m-0 whitespace-pre-wrap break-words border-0 bg-transparent p-0 font-mono text-xs font-semibold leading-5 text-content-primary">
|
||||
<pre className="m-0 whitespace-pre-wrap break-all border-0 bg-transparent p-0 font-mono text-xs font-semibold leading-5 text-content-primary">
|
||||
<span aria-hidden className="select-none">
|
||||
$
|
||||
</span>{" "}
|
||||
@@ -239,7 +239,7 @@ const ShellTranscriptBody: React.FC<{
|
||||
<pre
|
||||
key={block.kind}
|
||||
className={cn(
|
||||
"m-0 mt-4 whitespace-pre-wrap break-words border-0 bg-transparent p-0 font-mono text-xs font-normal leading-5",
|
||||
"m-0 mt-4 whitespace-pre-wrap break-all border-0 bg-transparent p-0 font-mono text-xs font-normal leading-5",
|
||||
block.kind === "error" || isError
|
||||
? "text-content-destructive"
|
||||
: "text-content-secondary",
|
||||
|
||||
@@ -29,7 +29,9 @@ const ReadFileContent: React.FC<{
|
||||
<ScrollArea
|
||||
className="mt-1.5 rounded-md border border-solid border-border-default text-2xs"
|
||||
viewportClassName="max-h-64"
|
||||
orientation="both"
|
||||
scrollBarClassName="w-1.5"
|
||||
horizontalScrollBarClassName="h-1.5"
|
||||
>
|
||||
<FileViewer
|
||||
file={{
|
||||
|
||||
@@ -2065,6 +2065,88 @@ export const GenericToolFailedNoResult: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
const longCodeLine =
|
||||
'export const config = { apiUrl: "https://coder.example.com/api/v2/workspaces", token: "abcdefghijklmnopqrstuvwxyz0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", retries: 5 };';
|
||||
|
||||
const tallWideFileContent = [
|
||||
longCodeLine,
|
||||
...Array.from({ length: 40 }, (_, i) => `const line${i} = ${i};`),
|
||||
].join("\n");
|
||||
|
||||
export const ReadFileLongLine: Story = {
|
||||
args: {
|
||||
name: "read_file",
|
||||
args: { path: "site/src/config.ts" },
|
||||
result: { content: longCodeLine },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
canvas.getByRole("button", { name: /Read config.ts/i }),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(getDiffsText(canvasElement)).toContain("apiUrl"),
|
||||
);
|
||||
await waitFor(() => {
|
||||
const host = canvasElement.querySelector("diffs-container");
|
||||
const code = host?.shadowRoot?.querySelector("[data-code]");
|
||||
expect(code).toBeInstanceOf(HTMLElement);
|
||||
if (code instanceof HTMLElement) {
|
||||
expect(getComputedStyle(code).overflow).toBe("visible");
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const ReadFileTallAndWide: Story = {
|
||||
args: {
|
||||
name: "read_file",
|
||||
args: { path: "site/src/config.ts" },
|
||||
result: { content: tallWideFileContent },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
canvas.getByRole("button", { name: /Read config.ts/i }),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(getDiffsText(canvasElement)).toContain("apiUrl"),
|
||||
);
|
||||
const viewport = [
|
||||
...canvasElement.querySelectorAll<HTMLElement>(
|
||||
"[data-radix-scroll-area-viewport]",
|
||||
),
|
||||
].find(
|
||||
(v) => v.scrollWidth > v.clientWidth && v.scrollHeight > v.clientHeight,
|
||||
);
|
||||
if (!viewport) {
|
||||
throw new Error("Expected a viewport overflowing on both axes.");
|
||||
}
|
||||
viewport.dispatchEvent(
|
||||
new WheelEvent("wheel", { deltaY: 200, bubbles: true, cancelable: true }),
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
expect(viewport.scrollLeft).toBe(0);
|
||||
},
|
||||
};
|
||||
|
||||
export const GenericToolLongOutput: Story = {
|
||||
args: {
|
||||
name: "some_custom_tool",
|
||||
args: { query: "lookup" },
|
||||
result: { value: longCodeLine },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
canvas.getByRole("button", { name: /some_custom_tool/i }),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(getDiffsText(canvasElement)).toContain("apiUrl"),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const SubagentWaitTimedOut: Story = {
|
||||
args: {
|
||||
name: "wait_agent",
|
||||
|
||||
@@ -832,7 +832,9 @@ const ToolFileViewer: FC<ToolFileViewerProps> = ({ label, file, options }) => (
|
||||
<ScrollArea
|
||||
className="mt-1.5 rounded-md border border-solid border-border-default text-2xs"
|
||||
viewportClassName="max-h-64"
|
||||
orientation="both"
|
||||
scrollBarClassName="w-1.5"
|
||||
horizontalScrollBarClassName="h-1.5"
|
||||
>
|
||||
<FileViewer file={file} options={options} style={DIFFS_FONT_STYLE} />
|
||||
</ScrollArea>
|
||||
|
||||
@@ -309,8 +309,10 @@ export const formatResultOutput = (result: unknown): string | null => {
|
||||
return formatValue(result);
|
||||
};
|
||||
|
||||
export const fileViewerCSS =
|
||||
"pre, [data-line], [data-diffs-header] { background-color: transparent !important; }";
|
||||
export const fileViewerCSS = [
|
||||
"pre, [data-line], [data-diffs-header] { background-color: transparent !important; }",
|
||||
"[data-code] { overflow: visible !important; }",
|
||||
].join(" ");
|
||||
|
||||
// Selection override CSS maps the library's gold/yellow selection
|
||||
// palette to the Coder blue accent (`--content-link`) so line
|
||||
|
||||
Reference in New Issue
Block a user