Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 17cbc26

Browse filesBrowse files
fix(site): replace ansi-to-html in the log viewer (#27206) (#27241)
Backport of #27206 Original PR: #27206 — fix(site): replace ansi-to-html in the log viewer Merge commit: b377bec Requested by: @aslilac Co-authored-by: McKayla はな <mckayla@hey.com>
1 parent f2653d4 commit 17cbc26
Copy full SHA for 17cbc26

5 files changed

+90-34Lines changed: 90 additions & 34 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎site/package.json‎

Copy file name to clipboardExpand all lines: site/package.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@
6767
"@xterm/addon-web-links": "0.12.0",
6868
"@xterm/addon-webgl": "0.19.0",
6969
"@xterm/xterm": "5.5.0",
70-
"ansi-to-html": "0.7.2",
7170
"axios": "1.16.1",
7271
"chroma-js": "2.6.0",
7372
"class-variance-authority": "0.7.1",
@@ -79,6 +78,7 @@
7978
"dayjs": "1.11.20",
8079
"diff": "8.0.4",
8180
"emoji-mart": "5.6.0",
81+
"fancy-ansi": "0.1.3",
8282
"file-saver": "2.0.5",
8383
"formik": "2.4.9",
8484
"front-matter": "4.0.2",
Collapse file

‎site/pnpm-lock.yaml‎

Copy file name to clipboardExpand all lines: site/pnpm-lock.yaml
+10-17Lines changed: 10 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Collapse file

‎site/src/modules/resources/AgentLogs/AgentLogLine.test.tsx‎

Copy file name to clipboardExpand all lines: site/src/modules/resources/AgentLogs/AgentLogLine.test.tsx
+33-4Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,50 @@ import type { Line } from "#/components/Logs/LogLine";
33
import { renderComponent } from "#/testHelpers/renderHelpers";
44
import { AgentLogLine } from "./AgentLogLine";
55

6-
const line: Line = {
6+
const makeLine = (output: string): Line => ({
77
id: 1,
88
level: "info",
9-
output: 'safe <span data-testid="agent-log-xss">xss</span>',
9+
output,
1010
sourceId: "source-id",
1111
time: "2024-03-14T11:31:04.090715Z",
12-
};
12+
});
13+
14+
const renderLine = (output: string) =>
15+
renderComponent(
16+
<AgentLogLine line={makeLine(output)} sourceIcon={null} style={{}} />,
17+
);
1318

1419
describe("AgentLogLine", () => {
1520
it("renders log HTML as escaped text", () => {
16-
renderComponent(<AgentLogLine line={line} sourceIcon={null} style={{}} />);
21+
renderLine('safe <span data-testid="agent-log-xss">xss</span>');
1722

1823
expect(screen.queryByTestId("agent-log-xss")).not.toBeInTheDocument();
1924
expect(
2025
screen.getByText(/safe <span data-testid="agent-log-xss">xss<\/span>/),
2126
).toBeInTheDocument();
2227
});
28+
29+
it("renders ANSI color codes as styled markup", () => {
30+
renderLine("\u001b[31mred\u001b[0m plain");
31+
32+
const colored = screen.getByText("red");
33+
expect(colored.tagName).toBe("SPAN");
34+
expect(colored.getAttribute("style")).toContain("--ansi-red");
35+
expect(screen.getByText(/plain/)).toBeInTheDocument();
36+
});
37+
38+
it("shows only the text after the last carriage return", () => {
39+
renderLine("downloading... 50%\rdownloading... 100%");
40+
41+
expect(screen.getByText("downloading... 100%")).toBeInTheDocument();
42+
expect(screen.queryByText(/50%/)).not.toBeInTheDocument();
43+
});
44+
45+
it("renders a ReDoS payload without hanging", () => {
46+
// Cure53 CDM-02-004: this pattern caused catastrophic backtracking in
47+
// ansi-to-html. fancy-ansi parses it in linear time.
48+
const start = performance.now();
49+
renderLine(`\u001b[${"1".repeat(50000)}`);
50+
expect(performance.now() - start).toBeLessThan(1000);
51+
});
2352
});
Collapse file
+9-12Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import AnsiToHTML from "ansi-to-html";
21
import dayjs from "dayjs";
2+
import { AnsiHtml } from "fancy-ansi/react";
33
import { type FC, type ReactNode, useMemo } from "react";
44
import { type Line, LogLine, LogLinePrefix } from "#/components/Logs/LogLine";
55
// Approximate height of a log line. Used to control virtualized list height.
66
export const AGENT_LOG_LINE_HEIGHT = 20;
77

8-
const convert = new AnsiToHTML({ escapeXML: true });
9-
108
interface AgentLogLineProps {
119
line: Line;
1210
style: React.CSSProperties;
@@ -18,9 +16,13 @@ export const AgentLogLine: FC<AgentLogLineProps> = ({
1816
sourceIcon,
1917
style,
2018
}) => {
21-
const output = useMemo(() => {
22-
return convert.toHtml(line.output.split(/\r/g).pop() as string);
23-
}, [line.output]);
19+
// Only render the text after the last carriage return so progress-bar style
20+
// output that redraws a single line shows its final state.
21+
const lastCarriageReturn = line.output.lastIndexOf("\r");
22+
const output =
23+
lastCarriageReturn === -1
24+
? line.output
25+
: line.output.slice(lastCarriageReturn + 1);
2426
const timestamp = useMemo(() => {
2527
return dayjs(line.time).format("HH:mm:ss.SSS");
2628
}, [line.time]);
@@ -29,12 +31,7 @@ export const AgentLogLine: FC<AgentLogLineProps> = ({
2931
<LogLine className="pl-4" level={line.level} style={style}>
3032
{sourceIcon}
3133
<LogLinePrefix>{timestamp}</LogLinePrefix>
32-
<span
33-
// biome-ignore lint/security/noDangerouslySetInnerHtml: Output contains HTML to represent ANSI-code formatting
34-
dangerouslySetInnerHTML={{
35-
__html: output,
36-
}}
37-
/>
34+
<AnsiHtml text={output} />
3835
</LogLine>
3936
);
4037
};
Collapse file

‎site/src/modules/resources/AgentLogs/AgentLogs.stories.tsx‎

Copy file name to clipboardExpand all lines: site/src/modules/resources/AgentLogs/AgentLogs.stories.tsx
+37Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Meta, StoryObj } from "@storybook/react-vite";
2+
import type { Line } from "#/components/Logs/LogLine";
23
import { AGENT_LOG_LINE_HEIGHT } from "./AgentLogLine";
34
import { AgentLogs } from "./AgentLogs";
45
import { MockLogs, MockSources } from "./mocks";
@@ -28,3 +29,39 @@ export const Overflowed: Story = {
2829
overflowed: true,
2930
},
3031
};
32+
33+
const sourceId = MockSources[0].id;
34+
35+
// Demonstrates how ANSI escape sequences and carriage-return progress-bar
36+
// redraws are rendered in the log viewer.
37+
const AnsiLogs: readonly Line[] = [
38+
{
39+
id: 1,
40+
level: "info",
41+
output:
42+
"\u001b[31mred\u001b[0m \u001b[32mgreen\u001b[0m \u001b[34mblue\u001b[0m \u001b[1mbold\u001b[0m \u001b[3mitalic\u001b[0m",
43+
time: "2024-03-14T11:31:04.090715Z",
44+
sourceId,
45+
},
46+
{
47+
id: 2,
48+
level: "info",
49+
output: "\u001b[43m\u001b[30m warning \u001b[0m background colors",
50+
time: "2024-03-14T11:31:04.090715Z",
51+
sourceId,
52+
},
53+
{
54+
id: 3,
55+
level: "info",
56+
output: "downloading... 50%\rdownloading... 100%",
57+
time: "2024-03-14T11:31:04.090715Z",
58+
sourceId,
59+
},
60+
];
61+
62+
export const AnsiFormatting: Story = {
63+
args: {
64+
logs: AnsiLogs,
65+
height: AnsiLogs.length * AGENT_LOG_LINE_HEIGHT,
66+
},
67+
};

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.