diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx
index 4af024d5..7c2733a3 100644
--- a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx
+++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx
@@ -120,6 +120,34 @@ describe("ContentViewer", () => {
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
+ it("renders plain text in a wrapping code block by default", () => {
+ const block: ContentBlock = { type: "text", text: "some long command" };
+ const { container } = renderWithMantine();
+ expect(container.querySelector(".mantine-Code-root")).toHaveAttribute(
+ "data-variant",
+ "wrapping",
+ );
+ });
+
+ it("renders plain text in a non-wrapping code block when wrap is false", () => {
+ const block: ContentBlock = { type: "text", text: "some long command" };
+ const { container } = renderWithMantine(
+ ,
+ );
+ const code = container.querySelector(".mantine-Code-root");
+ expect(code).toHaveAttribute("data-variant", "nowrap");
+ // Full value exposed on hover since it may be clipped with an ellipsis.
+ expect(code).toHaveAttribute("title", "some long command");
+ });
+
+ it("does not set a title tooltip when wrapping (default)", () => {
+ const block: ContentBlock = { type: "text", text: "some long command" };
+ const { container } = renderWithMantine();
+ expect(container.querySelector(".mantine-Code-root")).not.toHaveAttribute(
+ "title",
+ );
+ });
+
it("renders text as markdown when mimeType is text/markdown", () => {
const block: ContentBlock = {
type: "text",
diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx
index 8ab5a60b..be37a0fa 100644
--- a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx
+++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx
@@ -14,6 +14,19 @@ export interface ContentViewerProps {
* instead of as preformatted code.
*/
mimeType?: string;
+ /**
+ * Whether long plain-text content wraps onto multiple lines. When `false`,
+ * text is kept to a single line (overflow clipped with an ellipsis) so the
+ * viewer keeps a fixed height — used by hosts like the server card where the
+ * box height must stay constant regardless of command/URL length. The full
+ * value remains available via the copy button (and a native `title`
+ * tooltip). Defaults to `true`.
+ *
+ * Intended for single-line values only: `false` applies `white-space:
+ * nowrap`, which collapses embedded newlines (e.g. pretty-printed JSON) onto
+ * one line — don't pass it for multi-line content.
+ */
+ wrap?: boolean;
}
function formatJson(content: string): string {
@@ -66,6 +79,7 @@ export function ContentViewer({
block,
copyable = false,
mimeType,
+ wrap = true,
}: ContentViewerProps) {
switch (block.type) {
case "text": {
@@ -93,7 +107,14 @@ export function ContentViewer({
return (
-
+
{displayText}
{copyable && (
diff --git a/clients/web/src/components/groups/ServerCard/ServerCard.tsx b/clients/web/src/components/groups/ServerCard/ServerCard.tsx
index 864ccf6a..1c3c3482 100644
--- a/clients/web/src/components/groups/ServerCard/ServerCard.tsx
+++ b/clients/web/src/components/groups/ServerCard/ServerCard.tsx
@@ -202,6 +202,7 @@ export function ServerCard({
diff --git a/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx b/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx
index e0e31e1f..4a94fbfc 100644
--- a/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx
+++ b/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx
@@ -147,7 +147,17 @@ export function ServerListScreen({
const grid = (
diff --git a/clients/web/src/theme/Code.ts b/clients/web/src/theme/Code.ts
index 77609c59..7910c7f7 100644
--- a/clients/web/src/theme/Code.ts
+++ b/clients/web/src/theme/Code.ts
@@ -9,6 +9,14 @@ export const ThemeCode = Code.extend({
root.wordBreak = "break-all";
root.whiteSpace = "pre-wrap";
}
+ if (props.variant === "nowrap") {
+ // Single line, fixed height: clip overflow with an ellipsis rather than
+ // wrapping onto new lines (which would grow the container's height). The
+ // full value stays available via the adjacent copy button.
+ root.whiteSpace = "nowrap";
+ root.overflow = "hidden";
+ root.textOverflow = "ellipsis";
+ }
return { root };
},
});