feat: add assistant window controls

This commit is contained in:
ajnart committed 2026-08-14 11:32:11 +02:00
1 parent bb6e8e5c5a
commit 517d9320dc
3 files changed
+144 -15

No files matched your search

+2
View File
@@ -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.
@@ -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;
@@ -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 (
<>
<Group className={classes.panelHeader} justify="space-between" wrap="nowrap" gap="xs">
{onWindowClose && onMinimize && onToggleMaximize && (
<Group
component="fieldset"
className={classes.windowControls}
gap={4}
wrap="nowrap"
aria-label={t("windowControls")}
>
<Tooltip label={t("close")}>
<ActionIcon
className={`${classes.windowControl} ${classes.windowControlClose}`}
onClick={onWindowClose}
aria-label={t("close")}
>
<IconX size={12} stroke={2.4} />
</ActionIcon>
</Tooltip>
<Tooltip label={t("minimize")}>
<ActionIcon
className={`${classes.windowControl} ${classes.windowControlMinimize}`}
onClick={onMinimize}
aria-label={t("minimize")}
>
<IconMinus size={12} stroke={2.4} />
</ActionIcon>
</Tooltip>
<Tooltip label={maximized ? t("restore") : t("maximize")}>
<ActionIcon
className={`${classes.windowControl} ${classes.windowControlMaximize}`}
onClick={onToggleMaximize}
aria-label={maximized ? t("restore") : t("maximize")}
aria-pressed={maximized}
>
{maximized ? (
<IconArrowsMinimize size={11} stroke={2.4} />
) : (
<IconArrowsMaximize size={11} stroke={2.4} />
)}
</ActionIcon>
</Tooltip>
</Group>
)}
<Group className={classes.panelIdentity} gap="xs" wrap="nowrap">
<ThemeIcon variant="light" color="red" radius="xl">
<IconRobot size={18} />
@@ -2800,19 +2849,6 @@ export const AssistantConversationSurface = ({
</ActionIcon>
</Tooltip>
)}
{onMinimize && (
<Tooltip label={t("minimize")}>
<ActionIcon
className={classes.panelAction}
variant="subtle"
color="gray"
onClick={onMinimize}
aria-label={t("minimize")}
>
<IconMinus size={18} />
</ActionIcon>
</Tooltip>
)}
</Group>
</Group>
<AssistantQuestionPortalProvider target={questionPortalTarget}>
@@ -2889,6 +2925,7 @@ export const AssistantPanel = ({
}: AssistantPanelProps) => {
const t = useScopedI18n("common.assistant");
const previousFocusRef = useRef<HTMLElement | null>(null);
const [maximized, setMaximized] = useState(false);
useWindowEvent("keydown", (event) => {
if (opened && event.key === "Escape") onClose();
@@ -2928,7 +2965,7 @@ export const AssistantPanel = ({
/>
)}
{opened && (
<dialog className={classes.floatingPanel} aria-label={t("title")} open>
<dialog className={classes.floatingPanel} data-maximized={maximized || undefined} aria-label={t("title")} open>
<AssistantConversationSurface
isRunning={isRunning}
pendingAction={pendingAction}
@@ -2942,6 +2979,12 @@ export const AssistantPanel = ({
onReasoningChange={onReasoningChange}
autoFocusComposer
onMinimize={onClose}
onWindowClose={() => {
onDismissActivity();
onClose();
}}
onToggleMaximize={() => setMaximized((current) => !current)}
maximized={maximized}
/>
</dialog>
)}