diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts
index 0c827cc95f..ecf46c1b56 100644
--- a/desktop/playwright.config.ts
+++ b/desktop/playwright.config.ts
@@ -51,6 +51,7 @@ export default defineConfig({
         "**/home-collapsed-top-chrome.spec.ts",
         "**/thread-unread.spec.ts",
         "**/thread-reply-anchor-roleplay.spec.ts",
+        "**/threadpane-ultrawide.spec.ts",
         "**/animated-avatar.spec.ts",
         "**/reminders.spec.ts",
         "**/virtualization.spec.ts",
diff --git a/desktop/src/shared/hooks/useThreadPanelWidth.ts b/desktop/src/shared/hooks/useThreadPanelWidth.ts
index 1e734f6799..18e36d7f9f 100644
--- a/desktop/src/shared/hooks/useThreadPanelWidth.ts
+++ b/desktop/src/shared/hooks/useThreadPanelWidth.ts
@@ -2,17 +2,25 @@ import * as React from "react";
 
 import {
   AUXILIARY_PANEL_DEFAULT_WIDTH_PX,
-  AUXILIARY_PANEL_MAX_WIDTH_PX,
-  AUXILIARY_PANEL_MIN_WIDTH_PX,
+  clampAuxiliaryPanelWidth,
 } from "@/shared/layout/AuxiliaryPanel";
 
 const THREAD_PANEL_WIDTH_SESSION_KEY = "buzz.desktop.thread-panel-width";
 
+function getViewportWidth(): number {
+  return typeof window === "undefined" ? 0 : window.innerWidth;
+}
+
+/**
+ * Clamp the stored panel width for the current viewport.
+ *
+ * The upper bound grows with the viewport (see {@link clampAuxiliaryPanelWidth}) so
+ * the pane can expand on ultrawide displays. `AuxiliaryPanelShell` additionally
+ * clamps the rendered width to `calc(100% - MIN)` at paint time, so a stored width
+ * larger than the current viewport never collapses the main pane.
+ */
 function clampThreadPanelWidth(width: number): number {
-  return Math.max(
-    AUXILIARY_PANEL_MIN_WIDTH_PX,
-    Math.min(AUXILIARY_PANEL_MAX_WIDTH_PX, width),
-  );
+  return clampAuxiliaryPanelWidth(width, getViewportWidth());
 }
 
 function getInitialThreadPanelWidth(): number {
diff --git a/desktop/src/shared/layout/AuxiliaryPanel/index.ts b/desktop/src/shared/layout/AuxiliaryPanel/index.ts
index 5f108b91ff..72724302d2 100644
--- a/desktop/src/shared/layout/AuxiliaryPanel/index.ts
+++ b/desktop/src/shared/layout/AuxiliaryPanel/index.ts
@@ -25,4 +25,6 @@ export {
   AUXILIARY_PANEL_MAX_WIDTH_PX,
   AUXILIARY_PANEL_MIN_WIDTH_PX,
   AUXILIARY_PANEL_SINGLE_COLUMN_BREAKPOINT_PX,
+  clampAuxiliaryPanelWidth,
+  getAuxiliaryPanelMaxWidth,
 } from "@/shared/layout/auxiliaryPanelLayout";
diff --git a/desktop/src/shared/layout/auxiliaryPanelLayout.test.mjs b/desktop/src/shared/layout/auxiliaryPanelLayout.test.mjs
new file mode 100644
index 0000000000..08313f70aa
--- /dev/null
+++ b/desktop/src/shared/layout/auxiliaryPanelLayout.test.mjs
@@ -0,0 +1,37 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+  AUXILIARY_PANEL_MAX_WIDTH_PX,
+  AUXILIARY_PANEL_MIN_WIDTH_PX,
+  clampAuxiliaryPanelWidth,
+  getAuxiliaryPanelMaxWidth,
+} from "./auxiliaryPanelLayout.ts";
+
+test("max width falls back to the static cap on narrow viewports", () => {
+  // On viewports where `viewportWidth - MIN` is below the static cap, the floor wins.
+  assert.equal(getAuxiliaryPanelMaxWidth(900), AUXILIARY_PANEL_MAX_WIDTH_PX);
+  assert.equal(getAuxiliaryPanelMaxWidth(0), AUXILIARY_PANEL_MAX_WIDTH_PX);
+});
+
+test("max width grows with the viewport, reserving the main pane", () => {
+  assert.equal(
+    getAuxiliaryPanelMaxWidth(3440),
+    3440 - AUXILIARY_PANEL_MIN_WIDTH_PX,
+  );
+});
+
+test("clamp keeps width within [min, viewport-aware max]", () => {
+  // Below the floor clamps up to the min.
+  assert.equal(
+    clampAuxiliaryPanelWidth(100, 3440),
+    AUXILIARY_PANEL_MIN_WIDTH_PX,
+  );
+  // A wide drag is allowed on an ultrawide viewport.
+  assert.equal(clampAuxiliaryPanelWidth(2000, 3440), 2000);
+  // The same wide value is clamped down on a small viewport.
+  assert.equal(
+    clampAuxiliaryPanelWidth(2000, 900),
+    AUXILIARY_PANEL_MAX_WIDTH_PX,
+  );
+});
diff --git a/desktop/src/shared/layout/auxiliaryPanelLayout.ts b/desktop/src/shared/layout/auxiliaryPanelLayout.ts
index b267f589eb..6c574562b2 100644
--- a/desktop/src/shared/layout/auxiliaryPanelLayout.ts
+++ b/desktop/src/shared/layout/auxiliaryPanelLayout.ts
@@ -3,3 +3,29 @@ export const AUXILIARY_PANEL_MIN_WIDTH_PX = 300;
 export const AUXILIARY_PANEL_SINGLE_COLUMN_BREAKPOINT_PX =
   AUXILIARY_PANEL_MIN_WIDTH_PX * 2;
 export const AUXILIARY_PANEL_MAX_WIDTH_PX = 720;
+
+/**
+ * Upper bound for the auxiliary panel width clamp, given the current viewport width.
+ *
+ * On ultrawide displays the static {@link AUXILIARY_PANEL_MAX_WIDTH_PX} is too small,
+ * so the panel is allowed to grow with the viewport while always reserving at least
+ * {@link AUXILIARY_PANEL_MIN_WIDTH_PX} for the main pane. The static cap acts as a
+ * floor, so narrow viewports keep their existing behavior.
+ */
+export function getAuxiliaryPanelMaxWidth(viewportWidth: number): number {
+  return Math.max(
+    AUXILIARY_PANEL_MAX_WIDTH_PX,
+    viewportWidth - AUXILIARY_PANEL_MIN_WIDTH_PX,
+  );
+}
+
+/** Clamp a stored panel width into the allowed range for the current viewport. */
+export function clampAuxiliaryPanelWidth(
+  width: number,
+  viewportWidth: number,
+): number {
+  return Math.max(
+    AUXILIARY_PANEL_MIN_WIDTH_PX,
+    Math.min(getAuxiliaryPanelMaxWidth(viewportWidth), width),
+  );
+}
diff --git a/desktop/tests/e2e/threadpane-ultrawide.spec.ts b/desktop/tests/e2e/threadpane-ultrawide.spec.ts
new file mode 100644
index 0000000000..7057701624
--- /dev/null
+++ b/desktop/tests/e2e/threadpane-ultrawide.spec.ts
@@ -0,0 +1,121 @@
+import { expect, test } from "@playwright/test";
+
+import { TEST_IDENTITIES, installMockBridge } from "../helpers/bridge";
+
+// Ultrawide viewport: 3440px is a common 21:9 monitor width.
+const ULTRAWIDE = { width: 3440, height: 1440 };
+
+async function waitForMockLiveSubscription(
+  page: import("@playwright/test").Page,
+  channelName: string,
+) {
+  await expect
+    .poll(async () =>
+      page.evaluate(
+        ({ ch }) =>
+          (
+            window as Window & {
+              __BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: {
+                channelName: string;
+              }) => boolean;
+            }
+          ).__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({ channelName: ch }) ??
+          false,
+        { ch: channelName },
+      ),
+    )
+    .toBe(true);
+}
+
+async function emitMockReply(
+  page: import("@playwright/test").Page,
+  channelName: string,
+  content: string,
+  parentEventId: string,
+) {
+  await page.evaluate(
+    ({ ch, msg, parent, pubkey }) =>
+      (
+        window as Window & {
+          __BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
+            channelName: string;
+            content: string;
+            parentEventId?: string | null;
+            pubkey?: string;
+            createdAt?: number;
+          }) => unknown;
+        }
+      ).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
+        channelName: ch,
+        content: msg,
+        parentEventId: parent,
+        pubkey,
+        createdAt: Math.floor(Date.now() / 1000) - 10,
+      }),
+    {
+      ch: channelName,
+      msg: content,
+      parent: parentEventId,
+      pubkey: TEST_IDENTITIES.alice.pubkey,
+    },
+  );
+}
+
+async function openThread(page: import("@playwright/test").Page) {
+  await page.goto("/");
+  await page.getByTestId("channel-general").click();
+  await expect(page.getByTestId("chat-title")).toHaveText("general");
+  await waitForMockLiveSubscription(page, "general");
+
+  // Seed a reply so a thread summary row appears, then open it.
+  await emitMockReply(
+    page,
+    "general",
+    "Reply to welcome",
+    "mock-general-welcome",
+  );
+  const threadSummary = page.getByTestId("message-thread-summary").first();
+  await expect(threadSummary).toBeVisible();
+  await threadSummary.click();
+  await expect(page.getByTestId("message-thread-panel")).toBeVisible();
+}
+
+test.describe("thread pane on ultrawide monitors", () => {
+  test("expands well past the legacy 720px cap", async ({ page }) => {
+    await page.setViewportSize(ULTRAWIDE);
+    await installMockBridge(page);
+    await openThread(page);
+
+    const pane = page.getByTestId("message-thread-panel");
+    const handle = page.getByTestId("right-auxiliary-pane-resize-handle");
+
+    const beforeBox = await pane.boundingBox();
+    if (!beforeBox) throw new Error("thread panel not laid out");
+    // The panel opens at its default narrow width, far from the viewport edge.
+    expect(beforeBox.width).toBeLessThan(720);
+    await page.screenshot({
+      path: "test-results/threadpane-ultrawide-before.png",
+    });
+
+    // Drag the resize handle far to the left to widen the right-hand pane.
+    const handleBox = await handle.boundingBox();
+    if (!handleBox) throw new Error("resize handle not laid out");
+    const startX = handleBox.x + handleBox.width / 2;
+    const startY = handleBox.y + handleBox.height / 2;
+    await page.mouse.move(startX, startY);
+    await page.mouse.down();
+    // Move left in steps so pointermove fires repeatedly.
+    for (let x = startX; x >= 600; x -= 120) {
+      await page.mouse.move(x, startY);
+    }
+    await page.mouse.up();
+
+    const afterBox = await pane.boundingBox();
+    if (!afterBox) throw new Error("thread panel not laid out after resize");
+    // The pane is now far wider than the old 720px hard cap.
+    expect(afterBox.width).toBeGreaterThan(1200);
+    await page.screenshot({
+      path: "test-results/threadpane-ultrawide-after.png",
+    });
+  });
+});
