From 517d9320dc71bf157a2a3056005b6a237ea92bcd Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 10 Aug 2026 11:39:52 +0200 Subject: [PATCH] feat: add assistant window controls --- apps/docs/docs/management/assistant.mdx | 2 + .../assistant/assistant-panel.module.css | 86 ++++++++++++++++++- .../components/assistant/assistant-panel.tsx | 71 ++++++++++++--- 3 files changed, 144 insertions(+), 15 deletions(-) diff --git a/apps/docs/docs/management/assistant.mdx b/apps/docs/docs/management/assistant.mdx index f31619ff2..4d9624849 100644 --- a/apps/docs/docs/management/assistant.mdx +++ b/apps/docs/docs/management/assistant.mdx @@ -22,6 +22,8 @@ While a response is running, a compact activity bar appears at the bottom of the avatar shows a thinking indicator. The activity bar can be opened to follow the response. Completed responses add an unread count to the user menu until they are opened or dismissed. The expanded conversation has a minimize action that returns it to the activity bar without interrupting a response. +Its desktop-style window controls can also maximize the conversation to the available viewport or close it +without leaving an activity bar behind. Each control includes an icon, tooltip, and accessible label. The transcript follows streamed output while it is already at the bottom, including fast responses. Scrolling up releases that follow state until **Scroll to latest** is selected or the user returns to the bottom. diff --git a/apps/nextjs/src/components/assistant/assistant-panel.module.css b/apps/nextjs/src/components/assistant/assistant-panel.module.css index a19b4da2b..2de291799 100644 --- a/apps/nextjs/src/components/assistant/assistant-panel.module.css +++ b/apps/nextjs/src/components/assistant/assistant-panel.module.css @@ -117,6 +117,15 @@ animation: assistant-panel-in 180ms cubic-bezier(0.22, 1, 0.36, 1); } +.floatingPanel[data-maximized] { + inset: 1rem; + width: auto; + height: auto; + min-height: 0; + transform: none; + animation: none; +} + .widgetPanel { container-name: assistant-widget; container-type: size; @@ -150,6 +159,7 @@ } .panelHeader { + min-width: 0; min-height: 3.75rem; flex: 0 0 auto; padding: 0.65rem 0.75rem 0.65rem 0.9rem; @@ -166,9 +176,52 @@ } .panelActions { + min-width: 0; flex: 0 0 auto; } +.windowControls { + flex: 0 0 auto; + margin: 0; + padding: 0; + border: 0; +} + +.windowControl { + width: 1.75rem; + height: 1.75rem; + border: 0; + color: rgba(0, 0, 0, 0.72); + transition: + filter 160ms cubic-bezier(0.22, 1, 0.36, 1), + transform 160ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.windowControl:hover { + filter: brightness(0.94) saturate(1.08); +} + +.windowControl:active { + transform: scale(0.92); +} + +.windowControl:focus-visible { + outline: 2px solid var(--mantine-primary-color-filled); + outline-offset: 2px; +} + +.windowControlClose { + background: var(--mantine-color-red-6); +} + +.windowControlMinimize { + background: var(--mantine-color-yellow-5); +} + +.windowControlMaximize { + background: var(--mantine-color-green-6); +} + .panelAction { width: 2.625rem; height: 2.625rem; @@ -322,12 +375,15 @@ min-height: 0; flex: 1; flex-direction: column; + overflow-x: hidden; overflow-y: auto; } .messages { width: 100%; + min-width: 0; max-width: 44rem; + box-sizing: border-box; margin-inline: auto; padding: 0.75rem 1rem 1.5rem; } @@ -1276,6 +1332,7 @@ position: relative; display: flex; width: 100%; + min-width: 0; max-width: none; flex-direction: column; align-items: stretch; @@ -1368,11 +1425,15 @@ .composerFooter { min-width: 0; + max-width: 100%; + overflow: hidden; padding-inline: 0.2rem; } .composerControls { min-width: 0; + max-width: 100%; + flex: 1 1 auto; overflow: hidden; } @@ -1482,7 +1543,8 @@ inset-inline: 0; bottom: calc(100% + 0.45rem); max-height: min(22rem, 45dvh); - overflow: auto; + overflow-x: hidden; + overflow-y: auto; padding: 0.35rem; border: 1px solid var(--mantine-color-default-border); border-radius: var(--mantine-radius-md); @@ -1492,12 +1554,17 @@ .triggerList { display: grid; + min-width: 0; + max-width: 100%; gap: 0.15rem; } .triggerItem { display: flex; width: 100%; + min-width: 0; + max-width: 100%; + box-sizing: border-box; min-height: 2.75rem; align-items: center; gap: 0.55rem; @@ -1599,6 +1666,14 @@ min-height: min(20rem, calc(100dvh - 1rem - env(safe-area-inset-bottom, 0px))); } + .floatingPanel[data-maximized] { + inset: 0; + width: 100vw; + height: 100dvh; + min-height: 0; + border-radius: 0; + } + .panelHeader { min-height: 3.5rem; padding: 0.5rem; @@ -1642,6 +1717,14 @@ } @media (max-width: 30em) { + .panelIdentity > :first-child { + display: none; + } + + .panelIdentityText { + max-width: 7rem; + } + .historyButton { width: 2.625rem; max-width: 2.625rem; @@ -1726,6 +1809,7 @@ @media (prefers-reduced-motion: reduce) { .activityBar, .floatingPanel, + .windowControl, .disclosureIcon, .reasoningChevron { animation: none; diff --git a/apps/nextjs/src/components/assistant/assistant-panel.tsx b/apps/nextjs/src/components/assistant/assistant-panel.tsx index e7401e55c..a482eb7c9 100644 --- a/apps/nextjs/src/components/assistant/assistant-panel.tsx +++ b/apps/nextjs/src/components/assistant/assistant-panel.tsx @@ -78,6 +78,7 @@ import { IconApps, IconArrowUp, IconArrowsMaximize, + IconArrowsMinimize, IconAt, IconCheck, IconChevronDown, @@ -2731,6 +2732,9 @@ interface AssistantConversationSurfaceProps extends AssistantConversationControl pendingAction: AssistantPendingAction | undefined; onExpand?: () => void; onMinimize?: () => void; + onWindowClose?: () => void; + onToggleMaximize?: () => void; + maximized?: boolean; } export const AssistantConversationSurface = ({ @@ -2747,6 +2751,9 @@ export const AssistantConversationSurface = ({ onReasoningChange, onExpand, onMinimize, + onWindowClose, + onToggleMaximize, + maximized = false, }: AssistantConversationSurfaceProps) => { const t = useScopedI18n("common.assistant"); const reducedMotion = useReducedMotion(); @@ -2761,6 +2768,48 @@ export const AssistantConversationSurface = ({ return ( <> + {onWindowClose && onMinimize && onToggleMaximize && ( + + + + + + + + + + + + + + {maximized ? ( + + ) : ( + + )} + + + + )} @@ -2800,19 +2849,6 @@ export const AssistantConversationSurface = ({ )} - {onMinimize && ( - - - - - - )} @@ -2889,6 +2925,7 @@ export const AssistantPanel = ({ }: AssistantPanelProps) => { const t = useScopedI18n("common.assistant"); const previousFocusRef = useRef(null); + const [maximized, setMaximized] = useState(false); useWindowEvent("keydown", (event) => { if (opened && event.key === "Escape") onClose(); @@ -2928,7 +2965,7 @@ export const AssistantPanel = ({ /> )} {opened && ( - + { + onDismissActivity(); + onClose(); + }} + onToggleMaximize={() => setMaximized((current) => !current)} + maximized={maximized} /> )}