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
49 changes: 46 additions & 3 deletions extensions/cli/src/ui/MarkdownRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Text } from "ink";
import { Box, Text } from "ink";
import React from "react";

import {
Expand Down Expand Up @@ -84,6 +84,17 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = React.memo(
</Text>
),
},
{
// Match URLs - must come after markdown patterns to avoid conflicts
// Matches http://, https://, and angle-bracket URLs like <https://...>
// Uses a capturing group to match URL content, but we'll use match[0] for full text
regex: /<?(https?:\/\/[^\s<>]+)>?/g,
render: (content, key) => (
<Text key={key} color="cyan">
{content}
</Text>
),
},
];

const renderMarkdown = (text: string | null | undefined) => {
Expand Down Expand Up @@ -121,6 +132,7 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = React.memo(
index: number;
length: number;
content: string;
fullMatch: string;
render: (content: string, key: string) => React.ReactNode;
}> = [];

Expand All @@ -138,10 +150,14 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = React.memo(
);

if (!isInCodeBlock) {
// For URLs, use the full match to preserve angle brackets
// For other patterns, use capture groups as before
const isUrlPattern = pattern.regex.source.includes("https?");
allMatches.push({
index: match.index,
length: match[0].length,
content: match[2] || match[1], // Use second capture group for headings, first for others
content: isUrlPattern ? match[0] : match[2] || match[1], // Use full match for URLs, capture groups for others
fullMatch: match[0],
render: pattern.render,
});
}
Expand All @@ -154,6 +170,7 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = React.memo(
length: number;
type: "code" | "other";
content?: string;
fullMatch?: string;
render?: (content: string, key: string) => React.ReactNode;
language?: string;
code?: string;
Expand All @@ -170,6 +187,7 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = React.memo(
length: match.length,
type: "other" as const,
content: match.content,
fullMatch: match.fullMatch,
render: match.render,
})),
];
Expand Down Expand Up @@ -227,7 +245,32 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = React.memo(
return parts;
};

return <Text>{renderMarkdown(content)}</Text>;
const parts = renderMarkdown(content);

// Check if content has URLs by looking for cyan-colored Text elements
const hasUrls = parts.some(
(part) =>
React.isValidElement(part) && (part as any).props?.color === "cyan",
);

// Use Box layout when URLs are present to prevent them from breaking
// across lines. Each element (URL or text) becomes a separate flex item.
if (hasUrls) {
return (
<Box flexDirection="row" flexWrap="wrap">
{parts.map((part, idx) =>
React.isValidElement(part) ? (
part
) : (
<Text key={`text-${idx}`}>{part}</Text>
),
)}
</Box>
);
}

// No URLs: use original Text wrapping for backward compatibility
return <Text>{parts}</Text>;
},
);

Expand Down
93 changes: 93 additions & 0 deletions extensions/cli/src/ui/MarkdownRenderer.url.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { render } from "ink-testing-library";
import React from "react";
import { describe, expect, it } from "vitest";

import { MarkdownRenderer } from "./MarkdownRenderer.js";

describe("MarkdownRenderer - URL handling", () => {
it("should render URLs without breaking", () => {
const content =
"Check out this link: https://github.com/continuedev/continue/discussions/8240";

const { lastFrame } = render(<MarkdownRenderer content={content} />);
const frame = lastFrame();

// URL should be present and complete
expect(frame).toContain(
"https://github.com/continuedev/continue/discussions/8240",
);
// Frame should contain the prefix text
expect(frame).toContain("Check out this link:");
});

it("should handle angle-bracket URLs", () => {
const content =
"expectation would be the link <https://github.com/continuedev/continue/discussions/8240>";

const { lastFrame } = render(<MarkdownRenderer content={content} />);
const frame = lastFrame();

// URL should be extracted and rendered, with angle brackets preserved
expect(frame).toContain(
"https://github.com/continuedev/continue/discussions/8240",
);
});

it("should handle multiple URLs in the same content", () => {
const content =
"Check https://example.com and also https://github.com/continuedev/continue";

const { lastFrame } = render(<MarkdownRenderer content={content} />);
const frame = lastFrame();

expect(frame).toContain("https://example.com");
expect(frame).toContain("https://github.com/continuedev/continue");
});

it("should handle URLs with markdown text", () => {
const content =
"This is **bold text** and this is a link: https://continue.dev and more text.";

const { lastFrame } = render(<MarkdownRenderer content={content} />);
const frame = lastFrame();

expect(frame).toContain("bold text");
expect(frame).toContain("https://continue.dev");
});

it("should handle http URLs", () => {
const content = "Visit http://localhost:3000/api/endpoint";

const { lastFrame } = render(<MarkdownRenderer content={content} />);
const frame = lastFrame();

expect(frame).toContain("http://localhost:3000/api/endpoint");
});

it("should use Box layout when URLs are present", () => {
const content = "Link: https://example.com";

const { lastFrame } = render(<MarkdownRenderer content={content} />);
const frame = lastFrame();

// Should contain the URL
expect(frame).toContain("https://example.com");
// Should render without errors
expect(frame).toBeTruthy();
});

it("should not break URLs across lines", () => {
// This is more of an integration test - the actual line breaking
// depends on terminal width, but we can verify the structure
const content =
"A very long URL: https://github.com/continuedev/continue/discussions/8240/with/many/path/segments";

const { lastFrame } = render(<MarkdownRenderer content={content} />);
const frame = lastFrame();

// The URL should be present as a complete unit
expect(frame).toContain(
"https://github.com/continuedev/continue/discussions/8240/with/many/path/segments",
);
});
});
Loading