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,43 +0,0 @@
/**
* Core stubs for the right-rail Agents UI.
*
* The real implementations live in {@code proprietary/components/agents/AgentsPanel.tsx}
* and shadow these stubs via the {@code @app/*} alias cascade when the proprietary
* build is active. Core builds render nothing, so the right rail collapses to the
* tool list unchanged.
*/
/** Whether the right rail should reserve space for agents UI. False in core. */
export function useAgentsEnabled(): boolean {
return false;
}
/**
* Whether the agent chat panel is currently open. Core builds have no chat,
* so this always returns false. Proprietary builds bridge to the ChatContext.
*/
export function useAgentChatOpen(): boolean {
return false;
}
/** Inline "Agents" section rendered above the tool list in {@code ToolPicker}. */
export function AgentsSection() {
return null;
}
/**
* Icon-only agent button rendered in the collapsed (minimised) right rail.
* Returns null in core; proprietary renders the Stirling agent shortcut.
*/
export function AgentsCollapsedButton(_props: { onExpand: () => void }) {
return null;
}
/**
* Agents card rendered inside the fullscreen tool picker. Matches the visual
* language of the fullscreen category cards (gradient border, title, items).
* Returns null in core; proprietary renders the Stirling agent.
*/
export function AgentsFullscreenSection() {
return null;
}
@@ -7,12 +7,9 @@
export function useChat() {
return {
messages: [] as never[],
isOpen: false,
isLoading: false,
progress: null,
progressLog: [] as never[],
toggleOpen: () => {},
setOpen: (_open: boolean) => {},
sendMessage: async (_content: string) => {},
clearChat: () => {},
};
@@ -3,11 +3,6 @@ import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import { useIsMobile } from "@app/hooks/useIsMobile";
import { usePreferences } from "@app/contexts/PreferencesContext";
import { useWorkbenchBar } from "@app/contexts/WorkbenchBarContext";
import {
AgentsFullscreenSection,
useAgentChatOpen,
useAgentsEnabled,
} from "@app/components/agents/AgentsPanel";
import FullscreenToolSurface from "@app/components/tools/FullscreenToolSurface";
import { ToolId } from "@app/types/toolId";
import type { ToolPanelGeometry } from "@app/hooks/tools/useToolPanelGeometry";
@@ -16,13 +11,11 @@ import type { ToolPanelGeometry } from "@app/hooks/tools/useToolPanelGeometry";
export function useIsFullscreenExpanded(): boolean {
const { toolPanelMode, leftPanelView, readerMode } = useToolWorkflow();
const isMobile = useIsMobile();
const agentChatOpen = useAgentChatOpen();
return (
toolPanelMode === "fullscreen" &&
leftPanelView === "toolPicker" &&
!isMobile &&
!readerMode &&
!agentChatOpen
!readerMode
);
}
@@ -50,8 +43,6 @@ export function FullscreenToolPanel({ geometry }: FullscreenToolPanelProps) {
handleToolSelect,
} = useToolWorkflow();
const isMobile = useIsMobile();
const agentsEnabled = useAgentsEnabled();
const agentChatOpen = useAgentChatOpen();
const { setAllButtonsDisabled } = useWorkbenchBar();
const { preferences, updatePreference } = usePreferences();
@@ -59,8 +50,7 @@ export function FullscreenToolPanel({ geometry }: FullscreenToolPanelProps) {
toolPanelMode === "fullscreen" &&
leftPanelView === "toolPicker" &&
!isMobile &&
!readerMode &&
!agentChatOpen;
!readerMode;
useEffect(() => {
setAllButtonsDisabled(fullscreenExpanded);
@@ -94,9 +84,6 @@ export function FullscreenToolPanel({ geometry }: FullscreenToolPanelProps) {
}
onExitFullscreenMode={() => setToolPanelMode("sidebar")}
geometry={geometry}
agentsSlot={
agentsEnabled && !searchQuery ? <AgentsFullscreenSection /> : null
}
/>
);
}
@@ -25,8 +25,6 @@ interface FullscreenToolSurfaceProps {
onToggleDescriptions: () => void;
onExitFullscreenMode: () => void;
geometry: ToolPanelGeometry | null;
/** Optional agents block rendered above the tool list. */
agentsSlot?: React.ReactNode;
}
const FullscreenToolSurface = ({
@@ -41,7 +39,6 @@ const FullscreenToolSurface = ({
onToggleDescriptions,
onExitFullscreenMode: _onExitFullscreenMode,
geometry,
agentsSlot,
}: FullscreenToolSurfaceProps) => {
const { t } = useTranslation();
const surfaceRef = useRef<HTMLDivElement>(null);
@@ -92,9 +89,6 @@ const FullscreenToolSurface = ({
className="tool-panel__fullscreen-scroll"
offsetScrollbars
>
{agentsSlot && (
<div className="tool-panel__fullscreen-agents">{agentsSlot}</div>
)}
<FullscreenToolList
filteredTools={filteredTools}
searchQuery={searchQuery}
@@ -1,4 +1,4 @@
import { useMemo, useState, useRef, useCallback } from "react";
import { useMemo, useState } from "react";
import { ActionIcon } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
@@ -8,11 +8,6 @@ import rainbowStyles from "@app/styles/rainbow.module.css";
import { useIsMobile } from "@app/hooks/useIsMobile";
import ToolPanel from "@app/components/tools/ToolPanel";
import ToolSearch from "@app/components/tools/toolPicker/ToolSearch";
import {
AgentsCollapsedButton,
AgentsSection,
useAgentsEnabled,
} from "@app/components/agents/AgentsPanel";
import {
PoliciesCollapsedButton,
PoliciesSection,
@@ -21,8 +16,6 @@ import {
usePolicyDetailActive,
} from "@app/components/policies/PoliciesSidebar";
import { PolicyAutoRunController } from "@app/components/policies/PolicyAutoRunController";
import { useChat } from "@app/components/chat/ChatContext";
import { ChatPanel } from "@app/components/chat/ChatPanel";
import { useFavoriteToolItems } from "@app/hooks/tools/useFavoriteToolItems";
import { useToolSections } from "@app/hooks/useToolSections";
import type { SubcategoryGroup } from "@app/hooks/useToolSections";
@@ -41,16 +34,12 @@ import {
import { useToolPanelGeometry } from "@app/hooks/tools/useToolPanelGeometry";
import "@app/components/tools/ToolPanel.css";
const DEFAULT_CHAT_WIDTH_PX = 18.5 * 16; // 18.5rem in px
const MIN_CHAT_WIDTH_PX = 240;
const MAX_CHAT_WIDTH_PX = 720;
/**
* Right-side rail wrapping the tool panel.
*
* Owns the rail-level concerns: collapse/expand chrome, the AGENTS top label,
* the collapsed strip (agent button + favourite/recommended icon shortcuts),
* and the agents chat overlay. Fullscreen takeover lives in FullscreenToolPanel.
* Owns the rail-level concerns: collapse/expand chrome and the collapsed strip
* (favourite/recommended icon shortcuts). Fullscreen takeover lives in
* FullscreenToolPanel.
*/
export default function RightSidebar() {
const { t } = useTranslation();
@@ -77,7 +66,6 @@ export default function RightSidebar() {
favoriteTools,
} = useToolWorkflow();
const agentsEnabled = useAgentsEnabled();
const policiesEnabled = usePoliciesEnabled();
const rawPolicyDetailActive = usePolicyDetailActive();
const fullscreenExpanded = useIsFullscreenExpanded();
@@ -101,57 +89,6 @@ export default function RightSidebar() {
const [allToolsView, setAllToolsView] = useState(false);
const { isOpen: isChatOpen, setOpen: setChatOpen } = useChat();
const [chatWidthPx, setChatWidthPx] = useState(DEFAULT_CHAT_WIDTH_PX);
const [isChatDragging, setIsChatDragging] = useState(false);
const chatDragState = useRef<{ startX: number; startWidth: number } | null>(
null,
);
const handleChatClose = useCallback(() => {
withViewTransition(() => setChatOpen(false));
setChatWidthPx(DEFAULT_CHAT_WIDTH_PX);
}, [setChatOpen]);
const handleResizeChatPointerDown = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
e.preventDefault();
chatDragState.current = { startX: e.clientX, startWidth: chatWidthPx };
setIsChatDragging(true);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
const onMove = (ev: PointerEvent) => {
if (!chatDragState.current) return;
const delta = chatDragState.current.startX - ev.clientX;
setChatWidthPx(
Math.max(
MIN_CHAT_WIDTH_PX,
Math.min(
MAX_CHAT_WIDTH_PX,
chatDragState.current.startWidth + delta,
),
),
);
};
const cleanup = () => {
chatDragState.current = null;
setIsChatDragging(false);
document.body.style.removeProperty("cursor");
document.body.style.removeProperty("user-select");
window.removeEventListener("pointermove", onMove);
window.removeEventListener("pointerup", cleanup);
window.removeEventListener("pointercancel", cleanup);
};
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", cleanup);
window.addEventListener("pointercancel", cleanup);
},
[chatWidthPx],
);
const handleShowAllTools = () => {
withViewTransition(() => setAllToolsView(true));
};
@@ -186,12 +123,11 @@ export default function RightSidebar() {
policiesEnabled && !allToolsView && leftPanelView === "toolPicker";
// When Policies are shown, the search moves OUT of the header to sit between
// the Policies and Tools sections (separating them); otherwise it stays in the
// header. Show the header search when there's a close button, or when agents
// are off in the default tool-picker view (inline filtering).
// header. Show the header search when there's a close button, or in the
// default tool-picker view.
const showInlineSearch = showPolicies && !showCloseButton;
const showHeaderSearch =
!showInlineSearch &&
(showCloseButton || (!agentsEnabled && leftPanelView === "toolPicker"));
!showInlineSearch && (showCloseButton || leftPanelView === "toolPicker");
const handleHeaderBack = () => {
if (inToolView) {
@@ -224,13 +160,6 @@ export default function RightSidebar() {
? (toolRegistry[selectedToolKey as ToolId] ?? null)
: null;
// Agents header + section is hidden when:
// - the AI engine is off, or
// - the user is in the all-tools view, or
// - a specific tool is being rendered (leftPanelView ≠ "toolPicker").
const showAgents =
agentsEnabled && !allToolsView && leftPanelView === "toolPicker";
// The detail takeover replaces the tool list ONLY in the same default view —
// never over an open tool or the all-tools view (which must keep priority).
// A lingering selection is harmless: it stays hidden behind a tool and the
@@ -243,7 +172,6 @@ export default function RightSidebar() {
const computedWidth = () => {
if (isMobile) return "100%";
if (isChatOpen) return `${chatWidthPx}px`;
if (!isPanelVisible) return "3.5rem";
return expandedWidth;
};
@@ -279,41 +207,17 @@ export default function RightSidebar() {
ref={toolPanelRef}
data-sidebar="tool-panel"
data-tour={fullscreenExpanded ? undefined : "tool-panel"}
className={`tool-panel flex flex-col ${fullscreenExpanded ? "tool-panel--fullscreen-active" : isChatOpen ? "" : "overflow-hidden"} bg-[var(--bg-toolbar)] border-l border-[var(--border-subtle)] transition-all duration-300 ease-out ${
className={`tool-panel flex flex-col ${fullscreenExpanded ? "tool-panel--fullscreen-active" : "overflow-hidden"} bg-[var(--bg-toolbar)] border-l border-[var(--border-subtle)] transition-all duration-300 ease-out ${
isRainbowMode ? rainbowStyles.rainbowPaper : ""
} ${isMobile ? "h-full border-r-0" : "h-screen"} ${fullscreenExpanded ? "tool-panel--fullscreen" : ""}`}
style={{
width: computedWidth(),
padding: "0",
...(isChatDragging ? { transition: "none" } : {}),
}}
>
{/* Headless: enforces enabled policies on every uploaded file. */}
{policiesEnabled && <PolicyAutoRunController />}
{!fullscreenExpanded && isChatOpen && (
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
}}
>
<div
className="agents-takeover__resize-handle"
onPointerDown={handleResizeChatPointerDown}
role="separator"
aria-label={t("chat.resize", "Resize chat panel")}
aria-orientation="vertical"
/>
<ChatPanel
onBack={handleChatClose}
backLabel={t("agents.back_to_tools", "Back to tools")}
/>
</div>
)}
{!fullscreenExpanded && !isChatOpen && !isPanelVisible && !isMobile && (
{!fullscreenExpanded && !isPanelVisible && !isMobile && (
<div className="tool-panel__collapsed-strip">
<div className="tool-panel__collapsed-top">
<ActionIcon
@@ -327,7 +231,6 @@ export default function RightSidebar() {
>
<ChevronLeftIcon sx={{ fontSize: "1.1rem" }} />
</ActionIcon>
<AgentsCollapsedButton onExpand={handleExpand} />
</div>
<div className="tool-panel__collapsed-divider" />
<PoliciesCollapsedButton onExpand={handleOpenPolicy} />
@@ -358,7 +261,7 @@ export default function RightSidebar() {
</div>
)}
{!fullscreenExpanded && !isChatOpen && isPanelVisible && (
{!fullscreenExpanded && isPanelVisible && (
<div
/* Fixed width matches the expanded panel width so the inner content is
laid out at its final size from the moment it mounts. The outer
@@ -409,13 +312,7 @@ export default function RightSidebar() {
autoFocus={allToolsView && !inToolView}
/>
</div>
) : (
showAgents && (
<span className="tool-panel__section-label">
{t("agents.section_title", "Agents")}
</span>
)
)}
) : null}
{showCloseButton ? (
<ActionIcon
variant="subtle"
@@ -447,8 +344,6 @@ export default function RightSidebar() {
</div>
)}
{showAgents && <AgentsSection />}
{showPolicies && (
<PoliciesSection
leadingControl={
@@ -481,7 +376,7 @@ export default function RightSidebar() {
allToolsView={allToolsView}
onShowAllTools={handleShowAllTools}
onToolSelect={handleToolSelectWithTransition}
compact={agentsEnabled && !allToolsView}
compact={false}
/>
</>
)}
@@ -385,15 +385,6 @@
}
}
.tool-panel__section-label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
line-height: 1;
}
.tool-panel__mode-toggle {
transition: transform 0.2s ease;
}
@@ -855,33 +846,6 @@
gap: 0.75rem;
}
/* Agents card lives in its own column above the tool grid. It uses the same
.tool-panel__fullscreen-group base styles but with a colourful gradient
border so it stands out as a distinct surface (and doesn't blend into the
neighbouring category cards). */
.tool-panel__fullscreen-agents {
padding: 1.5rem 1.75rem 0;
}
.tool-panel__fullscreen-group--agents {
position: relative;
background:
linear-gradient(var(--fullscreen-bg-group), var(--fullscreen-bg-group))
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;
border: 1.5px solid transparent;
}
.tool-panel__fullscreen-section-icon--agents {
color: var(--mantine-color-blue-6);
}
@keyframes tool-panel-fullscreen-slide-in {
from {
transform: translateX(6%) scaleX(0.85);
@@ -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,
-5
View File
@@ -242,11 +242,6 @@ export default defineConfig(async ({ mode }) => {
},
},
},
resolve: {
alias: {
"@shared": path.resolve(__dirname, "../shared"),
},
},
optimizeDeps: {
exclude: ["@embedpdf/pdfium"],
},