diff --git a/site/src/pages/AgentsPage/AgentChatPageView.tsx b/site/src/pages/AgentsPage/AgentChatPageView.tsx index 478f208d00..84e0837a62 100644 --- a/site/src/pages/AgentsPage/AgentChatPageView.tsx +++ b/site/src/pages/AgentsPage/AgentChatPageView.tsx @@ -45,6 +45,7 @@ import { RightPanel } from "./components/RightPanel/RightPanel"; import { SidebarTabView } from "./components/Sidebar/SidebarTabView"; import { TerminalPanel } from "./components/TerminalPanel"; import { ChatWorkspaceContext } from "./context/ChatWorkspaceContext"; +import { chatWidthClass, useChatFullWidth } from "./hooks/useChatFullWidth"; import type { ChatDetailError } from "./utils/usageLimitMessage"; type ChatStoreHandle = ReturnType["store"]; @@ -552,6 +553,7 @@ export const AgentChatPageLoadingView: FC = ({ onToggleSidebarCollapsed, showRightPanel, }) => { + const [chatFullWidth] = useChatFullWidth(); return (
= ({ />
-
+
diff --git a/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx index 9528f89225..4793f61cd2 100644 --- a/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx @@ -1,5 +1,6 @@ import type { FC } from "react"; import type * as TypesGen from "#/api/typesGenerated"; +import { ChatFullWidthSettings } from "./components/ChatFullWidthSettings"; import { PersonalInstructionsSettings } from "./components/PersonalInstructionsSettings"; import { RetentionPeriodSettings } from "./components/RetentionPeriodSettings"; import { SectionHeader } from "./components/SectionHeader"; @@ -131,6 +132,9 @@ export const AgentSettingsBehaviorPageView: FC< isAnyPromptSaving={isAnyPromptSaving} /> +
+ +
= ({ onMCPAuthComplete, attachedWorkspace, }) => { + const [chatFullWidth] = useChatFullWidth(); const internalRef = useRef(null); const [previewImage, setPreviewImage] = useState(null); const [previewText, setPreviewText] = useState(null); @@ -612,7 +614,8 @@ export const AgentChatInput: FC = ({ const content = (
diff --git a/site/src/pages/AgentsPage/components/AgentsSkeletons.tsx b/site/src/pages/AgentsPage/components/AgentsSkeletons.tsx index 05a1847efa..e7fb815da1 100644 --- a/site/src/pages/AgentsPage/components/AgentsSkeletons.tsx +++ b/site/src/pages/AgentsPage/components/AgentsSkeletons.tsx @@ -1,6 +1,7 @@ import type { FC } from "react"; import { Skeleton } from "#/components/Skeleton/Skeleton"; import { cn } from "#/utils/cn"; +import { chatWidthClass, useChatFullWidth } from "../hooks/useChatFullWidth"; /** localStorage keys shared with the agents panel components. */ const RIGHT_PANEL_OPEN_KEY = "agents.right-panel-open"; @@ -126,9 +127,11 @@ export const RightPanelSkeleton: FC = () => ( * the real AgentChatInput so the transition from Suspense fallback to * the loaded component doesn't cause a vertical layout shift. */ -const ChatInputSkeleton: FC = () => ( +const ChatInputSkeleton: FC<{ fullWidth: boolean }> = ({ fullWidth }) => (
-
+
@@ -147,6 +150,7 @@ const ChatInputSkeleton: FC = () => ( */ export const AgentChatPageSkeleton: FC = () => { const rightPanel = getRightPanelState(); + const [chatFullWidth] = useChatFullWidth(); return (
{
-
+
- +
{rightPanel.open && (
{ + const [enabled, setEnabled] = useChatFullWidth(); + + return ( +
+

+ Chat Layout +

+
+

+ Use full-width layout for agent chat messages, removing the default + max-width constraint. +

+ setEnabled(Boolean(checked))} + aria-label="Full-width chat" + /> +
+
+ ); +}; diff --git a/site/src/pages/AgentsPage/components/ChatPageContent.tsx b/site/src/pages/AgentsPage/components/ChatPageContent.tsx index 9e4bea72a5..d1f30fba76 100644 --- a/site/src/pages/AgentsPage/components/ChatPageContent.tsx +++ b/site/src/pages/AgentsPage/components/ChatPageContent.tsx @@ -2,6 +2,8 @@ import { type FC, Profiler, type ReactNode, useEffect } from "react"; import { toast } from "sonner"; import type { UrlTransform } from "streamdown"; import type * as TypesGen from "#/api/typesGenerated"; +import { cn } from "#/utils/cn"; +import { chatWidthClass, useChatFullWidth } from "../hooks/useChatFullWidth"; import { useFileAttachments } from "../hooks/useFileAttachments"; import type { ChatDetailError } from "../utils/usageLimitMessage"; import { @@ -60,6 +62,7 @@ export const ChatPageTimeline: FC = ({ urlTransform, mcpServers, }) => { + const [chatFullWidth] = useChatFullWidth(); const messagesByID = useChatSelector(store, selectMessagesByID); const orderedMessageIDs = useChatSelector(store, selectOrderedMessageIDs); @@ -85,7 +88,10 @@ export const ChatPageTimeline: FC = ({
{/* VNC sessions for completed agents may already be terminated, so inline desktop previews are disabled diff --git a/site/src/pages/AgentsPage/hooks/useChatFullWidth.ts b/site/src/pages/AgentsPage/hooks/useChatFullWidth.ts new file mode 100644 index 0000000000..405e0f6c91 --- /dev/null +++ b/site/src/pages/AgentsPage/hooks/useChatFullWidth.ts @@ -0,0 +1,55 @@ +import { useSyncExternalStore } from "react"; + +const KEY = "agents.chat-full-width"; + +// In-tab subscribers. The native "storage" event only fires +// cross-tab, so we maintain our own listener set for same-tab +// reactivity when the toggle is flipped in settings. +const listeners = new Set<() => void>(); + +function subscribe(callback: () => void): () => void { + listeners.add(callback); + + // Cross-tab changes via the native storage event. + const onStorage = (e: StorageEvent) => { + if (e.key === KEY) { + callback(); + } + }; + window.addEventListener("storage", onStorage); + + return () => { + listeners.delete(callback); + window.removeEventListener("storage", onStorage); + }; +} + +function getSnapshot(): boolean { + return localStorage.getItem(KEY) === "true"; +} + +/** + * Returns the Tailwind max-width class for the chat layout + * based on whether full-width mode is enabled. + */ +export function chatWidthClass(fullWidth: boolean): string { + return fullWidth ? "max-w-full" : "max-w-3xl"; +} + +/** + * Reactive hook for the chat full-width preference. All + * consumers re-render when the value changes — no page reload + * required. + */ +export function useChatFullWidth(): [boolean, (v: boolean) => void] { + const enabled = useSyncExternalStore(subscribe, getSnapshot); + + const setEnabled = (value: boolean) => { + localStorage.setItem(KEY, String(value)); + for (const fn of listeners) { + fn(); + } + }; + + return [enabled, setEnabled]; +}