Kill off agents pane now that we have FAB (#6613)

# Description of Changes
Kill off agents pane now that we have the FAB. Also fixes a bug with the
FAB where it would sometimes fail to render the chat, and fixes a
duplicated entry in the Vite config which was throwing a warning
This commit is contained in:
James Brunton
2026-06-11 17:28:05 +01:00
committed by GitHub
parent 5bc7ae626d
commit 34ead60194
12 changed files with 58 additions and 1006 deletions
@@ -1,483 +0,0 @@
/* ─── Chat takeover ─────────────────────────────────────────────────────── */
.agents-takeover {
position: fixed;
right: 0;
top: 0;
bottom: 0;
display: flex;
flex-direction: column;
background: var(--bg-toolbar);
border-left: 1px solid var(--border-subtle);
view-transition-name: agents-rail;
}
.agents-takeover__resize-handle {
position: absolute;
left: -3px;
top: 0;
bottom: 0;
width: 7px;
cursor: col-resize;
z-index: 10;
touch-action: none;
}
.agents-takeover__resize-handle::after {
content: "";
position: absolute;
left: 3px;
top: 0;
bottom: 0;
width: 1px;
background: transparent;
transition:
background 150ms ease,
width 150ms ease,
left 150ms ease;
}
.agents-takeover__resize-handle:hover::after {
left: 2px;
width: 3px;
background: var(--mantine-color-blue-4);
opacity: 0.6;
}
/* ─── Sidebar agents section ─────────────────────────────────────────────── */
.agents-section {
padding: 0.5rem 0.75rem 0.875rem;
border-bottom: 1px solid var(--border-subtle);
background: var(--bg-toolbar);
flex-shrink: 0;
view-transition-name: agents-rail;
}
/* Hero button — real Stirling agent */
.agent-button {
display: block;
width: 100%;
padding: 0.75rem;
border: 1px solid
color-mix(in srgb, var(--mantine-color-blue-5) 40%, var(--border-subtle));
border-radius: 0.625rem;
background: linear-gradient(
135deg,
color-mix(
in srgb,
var(--mantine-color-blue-6) 6%,
var(--mantine-color-body)
)
0%,
var(--mantine-color-body) 100%
);
transition:
background 150ms ease-out,
border-color 150ms ease-out;
}
.agent-button:hover {
border-color: color-mix(
in srgb,
var(--mantine-color-blue-5) 65%,
var(--border-subtle)
);
background: linear-gradient(
135deg,
color-mix(
in srgb,
var(--mantine-color-blue-6) 12%,
var(--mantine-color-default-hover)
)
0%,
var(--mantine-color-default-hover) 100%
);
}
.agent-button__logo {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--mantine-color-blue-filled);
flex-shrink: 0;
position: relative;
}
/* Coming-soon agent rows */
.agents-sidebar-list {
display: flex;
flex-direction: column;
gap: 0.375rem;
margin-top: 0.375rem;
}
.agent-button--coming-soon {
display: block;
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-subtle);
border-radius: 0.625rem;
background: var(--mantine-color-body);
cursor: not-allowed !important;
opacity: 0.65;
transition:
opacity 180ms ease-out,
transform 180ms ease-out,
border-color 120ms ease-out;
}
@starting-style {
.agent-button--coming-soon {
opacity: 0;
transform: translateY(5px);
}
}
.agent-button--coming-soon:hover {
opacity: 0.85;
border-color: color-mix(
in srgb,
var(--mantine-color-gray-4) 60%,
var(--border-subtle)
);
background: var(--mantine-color-default-hover);
}
.agent-button__icon-plain {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
color: var(--mantine-color-dimmed);
flex-shrink: 0;
}
.agents-view-all {
all: unset;
display: block;
width: 100%;
margin-top: 0.375rem;
padding: 0.25rem 0.625rem;
font-size: 0.75rem;
font-weight: 500;
color: var(--mantine-color-dimmed);
text-align: center;
cursor: pointer;
opacity: 0.7;
border-radius: 0.375rem;
transition: opacity 120ms ease-out;
box-sizing: border-box;
}
.agents-view-all:hover {
opacity: 1;
}
/* Dark mode — sidebar */
[data-mantine-color-scheme="dark"] .agent-button--coming-soon {
background: transparent;
border-color: color-mix(
in srgb,
var(--mantine-color-dark-4) 60%,
transparent
);
}
[data-mantine-color-scheme="dark"] .agent-button--coming-soon:hover {
border-color: var(--mantine-color-dark-3);
background: var(--mantine-color-dark-6);
}
[data-mantine-color-scheme="dark"] .agent-button {
border-color: color-mix(
in srgb,
var(--mantine-color-blue-7) 55%,
var(--border-subtle)
);
background: linear-gradient(
135deg,
color-mix(in srgb, var(--mantine-color-blue-9) 35%, transparent) 0%,
transparent 100%
);
}
[data-mantine-color-scheme="dark"] .agent-button:hover {
border-color: color-mix(
in srgb,
var(--mantine-color-blue-5) 60%,
var(--border-subtle)
);
background: linear-gradient(
135deg,
color-mix(
in srgb,
var(--mantine-color-blue-8) 45%,
rgba(255, 255, 255, 0.04)
)
0%,
rgba(255, 255, 255, 0.04) 100%
);
}
[data-mantine-color-scheme="dark"] .agent-button__logo {
color: var(--mantine-color-blue-3, var(--mantine-color-blue-filled));
}
/* ─── Collapsed-rail agent button ────────────────────────────────────────── */
.agents-collapsed-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
border: none;
border-radius: 0.5rem;
background: transparent;
color: var(--mantine-color-blue-filled);
cursor: pointer;
transition: background 120ms ease-out;
position: relative;
}
.agents-collapsed-btn:hover {
background: var(--mantine-color-default-hover);
}
/* ─── Running / in-progress status dot ──────────────────────────────────── */
.agent-status-dot {
position: absolute;
bottom: 0;
right: 0;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--mantine-color-blue-5);
border: 1.5px solid var(--mantine-color-body);
animation: agent-dot-pulse 2.4s ease-in-out infinite;
pointer-events: none;
}
@keyframes agent-dot-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.45;
}
}
/* Blue outline on hero Stirling button when agent is running */
.agent-button.agent-button--running {
border-color: color-mix(
in srgb,
var(--mantine-color-blue-5) 60%,
var(--border-subtle)
);
}
[data-mantine-color-scheme="dark"] .agent-button.agent-button--running {
border-color: color-mix(
in srgb,
var(--mantine-color-blue-4) 55%,
var(--border-subtle)
);
}
@media (prefers-reduced-motion: reduce) {
.agent-status-dot {
animation: none;
}
}
/* ─── Fullscreen hero card ───────────────────────────────────────────────── */
/*
* .tool-panel__fullscreen-group--agents gives the gradient border via the
* padding-box / border-box trick. We only override the padding-box with a
* very light tint so the card doesn't read as a colourful surface.
*/
.agents-hero {
padding: 1.25rem 1.5rem 1.5rem;
border-radius: 1rem;
background:
linear-gradient(
125deg,
color-mix(in srgb, var(--mantine-color-blue-1) 35%, white) 0%,
color-mix(in srgb, var(--mantine-color-violet-1) 45%, white) 50%,
color-mix(in srgb, var(--mantine-color-pink-0) 35%, white) 100%
)
padding-box,
linear-gradient(
135deg,
var(--mantine-color-blue-6) 0%,
var(--mantine-color-violet-6) 45%,
var(--mantine-color-pink-6) 100%
)
border-box;
}
/* Body: left CTA + right grid */
.agents-hero__body {
display: flex;
gap: 2rem;
align-items: flex-start;
}
/* ── Left CTA — plain content, only the button inside is interactive ── */
.agents-hero__cta {
display: flex;
flex-direction: column;
gap: 0;
flex: 0 0 18rem;
box-sizing: border-box;
padding: 0.25rem 0;
}
.agents-hero__cta-logo {
color: var(--mantine-color-blue-filled);
margin-bottom: 0.625rem;
}
.agents-hero__cta-headline {
font-size: 1.375rem;
}
.agents-hero__cta-btn {
all: unset;
margin-top: 1.25rem;
display: inline-flex;
align-items: center;
padding: 0.4375rem 1rem;
border-radius: 0.5rem;
background: var(--mantine-color-blue-6);
color: white;
font-size: 0.8125rem;
font-weight: 600;
line-height: 1;
width: fit-content;
cursor: pointer;
transition: background 0.15s ease;
}
.agents-hero__cta-btn:hover {
background: var(--mantine-color-blue-7);
}
.agents-hero__cta-btn:focus-visible {
outline: 2px solid var(--mantine-color-blue-5);
outline-offset: 3px;
border-radius: 0.5rem;
}
/* ── Right 2×3 grid — sizes to content, pushed to the right ── */
.agents-hero__grid {
flex: 0 0 auto;
margin-left: auto;
display: grid;
/* auto columns: each column = widest item in that column */
grid-template-columns: repeat(2, auto);
gap: 0.625rem;
align-content: start;
align-self: start;
}
/* Solid white cards — no opacity so the white is opaque */
.agents-hero__grid-item {
all: unset;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0.875rem;
border: 1px solid rgba(0, 0, 0, 0.07);
border-radius: 0.75rem;
background: var(--mantine-color-body);
cursor: not-allowed;
box-sizing: border-box;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.agents-hero__grid-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--mantine-color-blue-5);
}
.agents-hero__grid-body {
min-width: 0;
flex: 1;
}
/* ── Dark mode — fullscreen ── */
[data-mantine-color-scheme="dark"] .agents-hero {
background:
linear-gradient(
125deg,
color-mix(in srgb, var(--mantine-color-blue-9) 55%, #1a1b2e) 0%,
color-mix(in srgb, var(--mantine-color-violet-9) 45%, #1a1b2e) 50%,
color-mix(in srgb, var(--mantine-color-pink-9) 40%, #1a1b2e) 100%
)
padding-box,
linear-gradient(
135deg,
var(--mantine-color-blue-6) 0%,
var(--mantine-color-violet-6) 45%,
var(--mantine-color-pink-6) 100%
)
border-box;
}
[data-mantine-color-scheme="dark"] .agents-hero__cta-logo {
color: var(--mantine-color-blue-3);
}
[data-mantine-color-scheme="dark"] .agents-hero__grid-item {
background: var(--fullscreen-bg-item);
border-color: var(--fullscreen-border-subtle-70);
box-shadow: none;
}
[data-mantine-color-scheme="dark"] .agents-hero__grid-icon {
color: var(--mantine-color-blue-3);
}
/* ─── agents-rail view transition — slide up ─────────────────────────────── */
::view-transition-old(agents-rail) {
animation: vt-agents-out 150ms ease-out forwards;
}
::view-transition-new(agents-rail) {
animation: vt-agents-in 260ms cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}
@keyframes vt-agents-out {
to {
opacity: 0;
}
}
@keyframes vt-agents-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
::view-transition-old(agents-rail),
::view-transition-new(agents-rail) {
animation-duration: 1ms !important;
}
}
@@ -1,275 +0,0 @@
import { useState } from "react";
import type { ElementType } from "react";
import { Box, Group, Text, UnstyledButton } from "@mantine/core";
import TableChartRoundedIcon from "@mui/icons-material/TableChartRounded";
import SummarizeRoundedIcon from "@mui/icons-material/SummarizeRounded";
import VisibilityOffRoundedIcon from "@mui/icons-material/VisibilityOffRounded";
import GavelRoundedIcon from "@mui/icons-material/GavelRounded";
import AssignmentRoundedIcon from "@mui/icons-material/AssignmentRounded";
import CodeRoundedIcon from "@mui/icons-material/CodeRounded";
import { useTranslation } from "react-i18next";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import { useChat } from "@app/components/chat/ChatContext";
import { Tooltip as AppTooltip } from "@app/components/shared/Tooltip";
import { StirlingLogoOutline } from "@app/components/agents/StirlingLogoOutline";
import { withViewTransition } from "@app/utils/viewTransition";
import "@app/components/agents/AgentsPanel.css";
interface ComingSoonAgent {
id: string;
nameKey: string;
descriptionKey: string;
Icon: ElementType;
}
const COMING_SOON_AGENTS: ComingSoonAgent[] = [
{
id: "data-extraction",
nameKey: "agents.data_extraction_name",
descriptionKey: "agents.data_extraction_description",
Icon: TableChartRoundedIcon,
},
{
id: "doc-summary",
nameKey: "agents.doc_summary_name",
descriptionKey: "agents.doc_summary_description",
Icon: SummarizeRoundedIcon,
},
{
id: "auto-redaction",
nameKey: "agents.auto_redaction_name",
descriptionKey: "agents.auto_redaction_description",
Icon: VisibilityOffRoundedIcon,
},
{
id: "compliance",
nameKey: "agents.compliance_name",
descriptionKey: "agents.compliance_description",
Icon: GavelRoundedIcon,
},
{
id: "form-filler",
nameKey: "agents.form_filler_name",
descriptionKey: "agents.form_filler_description",
Icon: AssignmentRoundedIcon,
},
{
id: "pdf-to-markdown",
nameKey: "agents.pdf_to_markdown_name",
descriptionKey: "agents.pdf_to_markdown_description",
Icon: CodeRoundedIcon,
},
];
export function useAgentsEnabled(): boolean {
const { config } = useAppConfig();
return Boolean(config?.aiEngineEnabled);
}
export function useAgentChatOpen(): boolean {
const { isOpen } = useChat();
return isOpen;
}
const PREVIEW_COUNT = 3;
/** Sidebar agents section — Stirling as hero CTA, coming-soon agents below. */
export function AgentsSection() {
const { t } = useTranslation();
const { isOpen, setOpen, isLoading } = useChat();
const enabled = useAgentsEnabled();
const [showAll, setShowAll] = useState(false);
if (!enabled || isOpen) return null;
const comingSoonLabel = t("agents.coming_soon", "Coming soon");
const visibleAgents = showAll
? COMING_SOON_AGENTS
: COMING_SOON_AGENTS.slice(0, PREVIEW_COUNT);
return (
<Box className="agents-section" w="100%">
{/* Main Stirling agent — real and clickable */}
<UnstyledButton
className={`agent-button agent-button--hero${isLoading ? " agent-button--running" : ""}`}
onClick={() => withViewTransition(() => setOpen(true))}
aria-label={t("agents.stirling_name", "Stirling")}
>
<Group gap="sm" wrap="nowrap" align="center">
<Box className="agent-button__logo">
<StirlingLogoOutline size={28} />
{isLoading && <span className="agent-status-dot" />}
</Box>
<Box style={{ minWidth: 0, flex: 1 }}>
<Text size="sm" fw={600} truncate>
{t("agents.stirling_name", "Stirling")}
</Text>
<Text size="xs" c="dimmed" truncate>
{isLoading
? t("agents.stirling_running", "Running...")
: t(
"agents.stirling_description",
"Your general-purpose PDF assistant",
)}
</Text>
</Box>
</Group>
</UnstyledButton>
{/* Coming-soon agents */}
<div className="agents-sidebar-list">
{visibleAgents.map(({ id, nameKey, descriptionKey, Icon }) => (
<AppTooltip
key={id}
content={comingSoonLabel}
position="left"
arrow
delay={0}
>
<UnstyledButton
className="agent-button agent-button--coming-soon"
aria-disabled="true"
tabIndex={-1}
>
<Group gap="sm" wrap="nowrap" align="center">
<Box className="agent-button__icon-plain">
<Icon sx={{ fontSize: "1.1rem" }} />
</Box>
<Box style={{ minWidth: 0, flex: 1 }}>
<Text size="sm" fw={500} truncate>
{t(nameKey)}
</Text>
<Text size="xs" c="dimmed" truncate>
{t(descriptionKey)}
</Text>
</Box>
</Group>
</UnstyledButton>
</AppTooltip>
))}
</div>
{!showAll ? (
<button
type="button"
className="agents-view-all"
onClick={() => withViewTransition(() => setShowAll(true))}
>
{t("agents.view_all", "View all agents")}
</button>
) : (
<button
type="button"
className="agents-view-all"
onClick={() => withViewTransition(() => setShowAll(false))}
>
{t("agents.show_less", "Show less")}
</button>
)}
</Box>
);
}
/** Icon-only agent button in the collapsed (minimised) right rail. */
export function AgentsCollapsedButton({ onExpand }: { onExpand: () => void }) {
const { t } = useTranslation();
const { setOpen, isLoading } = useChat();
const enabled = useAgentsEnabled();
if (!enabled) return null;
const label = t("agents.stirling_tooltip", "Stirling agent");
return (
<AppTooltip content={label} position="left" arrow delay={300}>
<UnstyledButton
onClick={() => {
onExpand();
setOpen(true);
}}
aria-label={label}
className="agents-collapsed-btn"
>
<StirlingLogoOutline size={22} />
{isLoading && <span className="agent-status-dot" />}
</UnstyledButton>
</AppTooltip>
);
}
/**
* Fullscreen hero card — Stirling CTA on the left, 2×3 coming-soon grid on
* the right. Matches the gradient border of the other fullscreen category cards.
*/
export function AgentsFullscreenSection() {
const { t } = useTranslation();
const { isOpen, setOpen } = useChat();
const enabled = useAgentsEnabled();
if (!enabled || isOpen) return null;
const comingSoonLabel = t("agents.coming_soon", "Coming soon");
return (
<section
className="agents-hero tool-panel__fullscreen-group--agents"
aria-label={t("agents.section_title", "Agents")}
>
<div className="agents-hero__body">
{/* Left: Stirling content — only the button is interactive */}
<div className="agents-hero__cta">
<div className="agents-hero__cta-logo">
<StirlingLogoOutline size={36} />
</div>
<Text className="agents-hero__cta-headline" fw={700} lh={1.2} mt="xs">
{t("agents.stirling_full_name", "Stirling General Agent")}
</Text>
<Text size="sm" c="dimmed" mt={8} lh={1.55}>
{t(
"agents.stirling_long_description",
"General purpose PDF assistant that can run tools, create PDFs and extract insights from your documents.",
)}
</Text>
<button
type="button"
className="agents-hero__cta-btn"
onClick={() => withViewTransition(() => setOpen(true))}
>
{t("agents.start_chat", "Start chatting")}
</button>
</div>
{/* Right: 2×3 grid of coming-soon agents */}
<div className="agents-hero__grid">
{COMING_SOON_AGENTS.map(({ id, nameKey, descriptionKey, Icon }) => (
<AppTooltip
key={id}
content={comingSoonLabel}
position="top"
arrow
delay={0}
>
<button
type="button"
className="agents-hero__grid-item"
aria-disabled="true"
>
<span className="agents-hero__grid-icon">
<Icon sx={{ fontSize: "1rem" }} />
</span>
<div className="agents-hero__grid-body">
<Text size="sm" fw={500} truncate>
{t(nameKey)}
</Text>
<Text size="xs" c="dimmed" truncate>
{t(descriptionKey)}
</Text>
</div>
</button>
</AppTooltip>
))}
</div>
</div>
</section>
);
}
@@ -183,7 +183,6 @@ interface AiWorkflowResponse {
interface ChatState {
messages: ChatMessage[];
isOpen: boolean;
isLoading: boolean;
progress: AiWorkflowProgress | null;
/** Ordered log of every progress event in the current request. UI shows the last N entries. */
@@ -200,8 +199,6 @@ type ChatAction =
| { type: "SET_LOADING"; loading: boolean }
| { type: "SET_PROGRESS"; progress: AiWorkflowProgress | null }
| { type: "APPEND_PROGRESS"; progress: AiWorkflowProgress }
| { type: "TOGGLE_OPEN" }
| { type: "SET_OPEN"; open: boolean }
| { type: "CLEAR" };
function chatReducer(state: ChatState, action: ChatAction): ChatState {
@@ -231,10 +228,6 @@ function chatReducer(state: ChatState, action: ChatAction): ChatState {
action.progress,
],
};
case "TOGGLE_OPEN":
return { ...state, isOpen: !state.isOpen };
case "SET_OPEN":
return { ...state, isOpen: action.open };
case "CLEAR":
return {
...state,
@@ -363,13 +356,10 @@ async function consumeSSEStream(
interface ChatContextValue {
messages: ChatMessage[];
isOpen: boolean;
isLoading: boolean;
progress: AiWorkflowProgress | null;
/** Ordered log of every progress event for the current in-flight request. */
progressLog: AiWorkflowProgress[];
toggleOpen: () => void;
setOpen: (open: boolean) => void;
sendMessage: (content: string) => Promise<void>;
/** Abort any in-flight request and reset the chat to an empty conversation. */
clearChat: () => void;
@@ -379,7 +369,6 @@ const ChatContext = createContext<ChatContextValue | null>(null);
const initialState: ChatState = {
messages: [],
isOpen: false,
isLoading: false,
progress: null,
progressLog: [],
@@ -465,11 +454,6 @@ export function ChatProvider({ children }: { children: ReactNode }) {
[fileActions, downloadFile],
);
const toggleOpen = useCallback(() => dispatch({ type: "TOGGLE_OPEN" }), []);
const setOpen = useCallback(
(open: boolean) => dispatch({ type: "SET_OPEN", open }),
[],
);
const clearChat = useCallback(() => {
abortRef.current?.abort();
abortRef.current = null;
@@ -649,12 +633,9 @@ export function ChatProvider({ children }: { children: ReactNode }) {
<ChatContext.Provider
value={{
messages: state.messages,
isOpen: state.isOpen,
isLoading: state.isLoading,
progress: state.progress,
progressLog: state.progressLog,
toggleOpen,
setOpen,
sendMessage,
clearChat,
}}
@@ -6,7 +6,7 @@ import { ChatFABButton } from "@shared/components/ChatFABButton";
import { ChatFABWindow } from "@shared/components/ChatFABWindow";
import { ChatPanel } from "@app/components/chat/ChatPanel";
import { useChat } from "@app/components/chat/ChatContext";
import { useAgentsEnabled } from "@app/components/agents/AgentsPanel";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import { Z_INDEX_CHAT_FAB_OVERLAY } from "@app/styles/zIndex";
import "@app/components/chat/ChatFAB.css";
@@ -54,7 +54,8 @@ export function ChatFAB() {
const [isOpen, setIsOpen] = useState(false);
const [hasUnviewedResult, setHasUnviewedResult] = useState(false);
const { isLoading } = useChat();
const enabled = useAgentsEnabled();
const { config } = useAppConfig();
const enabled = Boolean(config?.aiEngineEnabled);
// Detect loading → done transition. If the FAB is closed when the agent
// finishes, show the tick badge until the user opens the panel.
@@ -90,9 +91,13 @@ export function ChatFAB() {
};
useLayoutEffect(() => {
// The overlay only mounts once the AI engine is enabled (config can load
// after first render), so re-measure when that flips true rather than only
// on initial mount, otherwise the default position never gets computed.
if (!enabled) return;
const pos = getDefaultPos();
if (pos) setRndPos(pos);
}, []);
}, [enabled]);
// Clear the reset timer on unmount to avoid state updates on dead components.
// Also ensure body user-select is restored if we unmount mid-resize.
@@ -140,6 +145,12 @@ export function ChatFAB() {
<ChatFABButton
className={`chat-fab-trigger${isOpen ? " chat-fab-trigger--hidden" : ""}`}
onClick={() => {
// Fallback: ensure a position exists before opening, in case the
// layout effect measured before the overlay was laid out.
if (rndPos === null) {
const pos = getDefaultPos();
if (pos) setRndPos(pos);
}
setIsOpen(true);
setHasUnviewedResult(false);
}}
@@ -35,7 +35,6 @@
transition:
background 120ms ease-out,
border-color 120ms ease-out;
/* Pair with .agent-button to morph between the card and the pill. */
view-transition-name: stirling-agent;
}
@@ -64,6 +63,36 @@
position: relative;
}
/* Running / in-progress status dot, anchored to the agent pill icon. */
.agent-status-dot {
position: absolute;
bottom: 0;
right: 0;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--mantine-color-blue-5);
border: 1.5px solid var(--mantine-color-body);
animation: agent-dot-pulse 2.4s ease-in-out infinite;
pointer-events: none;
}
@keyframes agent-dot-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.45;
}
}
@media (prefers-reduced-motion: reduce) {
.agent-status-dot {
animation: none;
}
}
.chat-panel__agent-pill--loading {
border-color: color-mix(
in srgb,