diff --git a/apps/agor-ui/src/components/App/App.tsx b/apps/agor-ui/src/components/App/App.tsx index 76dca333a..255d6f63b 100644 --- a/apps/agor-ui/src/components/App/App.tsx +++ b/apps/agor-ui/src/components/App/App.tsx @@ -19,8 +19,7 @@ import type { User, } from '@agor-live/client'; import { hasMinimumRole, PermissionScope } from '@agor-live/client'; -import { LeftOutlined, RightOutlined } from '@ant-design/icons'; -import { Layout, Tooltip, Upload } from 'antd'; +import { Layout, Upload } from 'antd'; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { type ImperativePanelHandle, @@ -70,7 +69,7 @@ import { hasExplicitEntityRouteTarget } from '../../utils/routeTargets'; import { startAssistantBootstrapSession } from '../../utils/startAssistantBootstrapSession'; import { AppHeader } from '../AppHeader'; import type { BoardAssistantPanelTab } from '../BoardAssistantPanel'; -import { BoardAssistantPanel } from '../BoardAssistantPanel'; +import { AssistantPanelRail, BoardAssistantPanel } from '../BoardAssistantPanel'; import { BranchModal, type BranchModalTab } from '../BranchModal'; import type { BranchUpdate } from '../BranchModal/tabs/GeneralTab'; import { CreateDialog, type CreateDialogProgress } from '../CreateDialog'; @@ -87,7 +86,17 @@ import { SessionSettingsModal } from '../SessionSettingsModal'; import { SettingsModal, UserSettingsModal } from '../SettingsModal'; import { TerminalModal, WEB_TERMINAL_MIN_ROLE } from '../TerminalModal'; import { ThemeEditorModal } from '../ThemeEditorModal'; -import { getShowCommentsPanelState, getToggleBoardPanelState } from './boardPanelActions'; +import { + getSelectAssistantPanelTabState, + getShowCommentsPanelState, + getToggleBoardPanelState, +} from './boardPanelActions'; +import { + capSessionSizeForCanvasMin, + getContentPanelWidthPercent, + toContentRelativePercent, + toViewportRelativePercent, +} from './panelSizing'; const { Content } = Layout; @@ -210,12 +219,20 @@ const LEFT_PANEL_MAX_SIZE_PERCENT = 45; const SESSION_PANEL_MIN_WIDTH_PX = 360; const SESSION_PANEL_MAX_SIZE_PERCENT = 75; const SESSION_PANEL_MIN_SIZE_FLOOR_PERCENT = 15; -const LEFT_PANEL_TOGGLE_HIT_SIZE_PX = 44; -const LEFT_PANEL_TOGGLE_KNOB_SIZE_PX = 30; +// Matches the canvas panel's own `minSize` below — kept as one constant so +// the two cannot drift apart. +const CANVAS_MIN_SIZE_PERCENT = 20; +// Width of the persistent icon rail (AssistantPanelRail) shown in place of +// the panel when collapsed. Replaces the old 0px-collapse + floating +// reopen-knob pattern (see issue agor-cloud#123). +const LEFT_PANEL_RAIL_WIDTH_PX = 56; const getLeftPanelMinSizePercent = (viewportWidth: number) => Math.min(LEFT_PANEL_MAX_SIZE_PERCENT, (LEFT_PANEL_MIN_WIDTH_PX / viewportWidth) * 100); +const getLeftPanelRailSizePercent = (viewportWidth: number) => + (LEFT_PANEL_RAIL_WIDTH_PX / viewportWidth) * 100; + // Express the session panel's 360px minimum through the panel sizing system // (a percentage of the current viewport) rather than a CSS px `minWidth`, which // fights react-resizable-panels' percentage layout on narrow viewports. @@ -405,6 +422,11 @@ export const App: React.FC = ({ [viewportWidth] ); + const leftPanelRailSize = useMemo( + () => getLeftPanelRailSizePercent(viewportWidth), + [viewportWidth] + ); + const sessionPanelMinSize = useMemo( () => getSessionPanelMinSizePercent(viewportWidth), [viewportWidth] @@ -509,6 +531,11 @@ export const App: React.FC = ({ isHomeSurface || isLeavingHomeSurface || homeExitSidePanelDeferred; + // The rail only makes sense when there's a board to open the panel onto, + // and stays hidden entirely while a modal-first flow suppresses the panel + // (suppressLeftPanel) — same gating the old floating knob used. + const leftPanelRailVisible = leftPanelCollapsed && !!currentBoard && !suppressLeftPanel; + const leftPanelCollapsedSize = leftPanelRailVisible ? leftPanelRailSize : 0; // Ref for programmatically controlling the comments panel const commentsPanelRef = useRef(null); @@ -518,7 +545,23 @@ export const App: React.FC = ({ LEFT_PANEL_MAX_SIZE_PERCENT ); - // Session panel size persistence (percentage of available width), scoped per user. + // Width of the middle content panel (canvas + session panel), as a + // percentage of the full viewport — whatever's left once the left + // assistant/comments panel (rail or fully expanded) takes its share. The + // session panel's own size is persisted relative to the viewport (below), + // so this is the conversion factor used to translate that into the + // content-relative percentage react-resizable-panels expects — keeping the + // session panel's absolute pixel width stable whenever the left panel + // toggles or resizes. + const contentPanelWidthPercent = getContentPanelWidthPercent( + leftPanelCollapsed, + leftPanelCollapsedSize, + effectiveCommentsPanelSize + ); + + // Session panel size persistence: percentage of the FULL VIEWPORT (not of + // the content panel), scoped per user, so the chat panel's absolute pixel + // width doesn't change when the left panel collapses to a rail or back. const [sessionPanelSize, setSessionPanelSize] = useUserLocalStorage( user?.user_id, 'panel:right:size', @@ -530,6 +573,22 @@ export const App: React.FC = ({ sessionPanelMinSize, SESSION_PANEL_MAX_SIZE_PERCENT ); + // react-resizable-panels sizes the nested `canvas-session` PanelGroup's + // panels relative to the content panel, not the viewport — convert. Both + // the default and max are further capped so the canvas panel can always + // keep its own `minSize` (CANVAS_MIN_SIZE_PERCENT). + const sessionPanelSizeWithinContent = capSessionSizeForCanvasMin( + toContentRelativePercent(effectiveSessionPanelSize, contentPanelWidthPercent), + CANVAS_MIN_SIZE_PERCENT + ); + const sessionPanelMaxSizeWithinContent = capSessionSizeForCanvasMin( + toContentRelativePercent(SESSION_PANEL_MAX_SIZE_PERCENT, contentPanelWidthPercent), + CANVAS_MIN_SIZE_PERCENT + ); + const sessionPanelMinSizeWithinContent = Math.min( + toContentRelativePercent(sessionPanelMinSize, contentPanelWidthPercent), + sessionPanelMaxSizeWithinContent + ); const sessionPanelRef = useRef(null); const leftPanelResizeDraggingRef = useRef(false); const rightPanelResizeDraggingRef = useRef(false); @@ -590,11 +649,17 @@ export const App: React.FC = ({ } }, [effectiveCommentsPanelSize, leftPanelCollapsed]); + // Re-resize whenever the content-relative size changes — including when + // the left panel toggles or resizes and shifts contentPanelWidthPercent, + // which sessionPanelSizeWithinContent is derived from. Without this, the + // session panel keeps the same content-relative percentage while its + // parent's absolute width changes, so its own absolute pixel width would + // drift along with the left panel. useEffect(() => { if (sessionPanelRef.current && (effectiveSelectedSessionId || !eventStreamPanelCollapsed)) { - sessionPanelRef.current.resize(effectiveSessionPanelSize); + sessionPanelRef.current.resize(sessionPanelSizeWithinContent); } - }, [effectiveSelectedSessionId, effectiveSessionPanelSize, eventStreamPanelCollapsed]); + }, [effectiveSelectedSessionId, sessionPanelSizeWithinContent, eventStreamPanelCollapsed]); // URL state synchronization - bidirectional sync between URL and state useUrlState({ @@ -737,6 +802,15 @@ export const App: React.FC = ({ applyLeftPanelState(getShowCommentsPanelState({ collapsed: true, activeTab: 'assistant' })); }, [applyLeftPanelState]); + // Shared by every AssistantPanelRail button: expand the panel onto + // whichever tab was clicked. + const handleSelectAssistantPanelTab = useCallback( + (tab: BoardAssistantPanelTab) => { + applyLeftPanelState(getSelectAssistantPanelTabState(tab)); + }, + [applyLeftPanelState] + ); + const handleCommentSelect = useCallback((commentId: string | null) => { // Toggle selection: if clicking same comment, deselect setSelectedCommentId((prev) => (prev === commentId ? null : commentId)); @@ -1040,6 +1114,11 @@ export const App: React.FC = ({ ), [commentById, currentBoardId] ); + // Shared between AppHeader's comments button and the collapsed rail's + // Comments item so both surfaces carry the same badge. + const unreadCommentsCount = activeComments.filter( + (c: BoardComment) => !c.parent_comment_id + ).length; const currentUserName = user?.name || user?.email?.split('@')[0] || ''; const hasUserMentions = @@ -1190,9 +1269,7 @@ export const App: React.FC = ({ onRetryConnection={stableOnRetryConnection} currentBoardName={headerBoard?.name} currentBoardIcon={headerBoard?.icon} - unreadCommentsCount={ - activeComments.filter((c: BoardComment) => !c.parent_comment_id).length - } + unreadCommentsCount={unreadCommentsCount} eventStreamEnabled={eventStreamEnabled} hasUserMentions={hasUserMentions} currentBoardId={headerBoardId} @@ -1223,13 +1300,27 @@ export const App: React.FC = ({ order={1} ref={commentsPanelRef} collapsible - defaultSize={leftPanelCollapsed ? 0 : effectiveCommentsPanelSize} - collapsedSize={0} - minSize={leftPanelCollapsed ? 0 : leftPanelMinSize} + defaultSize={leftPanelCollapsed ? leftPanelCollapsedSize : effectiveCommentsPanelSize} + collapsedSize={leftPanelCollapsedSize} + minSize={leftPanelCollapsed ? leftPanelCollapsedSize : leftPanelMinSize} maxSize={LEFT_PANEL_MAX_SIZE_PERCENT} - style={{ minWidth: leftPanelCollapsed ? 0 : LEFT_PANEL_MIN_WIDTH_PX }} + style={{ + minWidth: leftPanelCollapsed + ? leftPanelRailVisible + ? LEFT_PANEL_RAIL_WIDTH_PX + : 0 + : LEFT_PANEL_MIN_WIDTH_PX, + }} > - {!leftPanelCollapsed && ( + {leftPanelCollapsed ? ( + leftPanelRailVisible && ( + + ) + ) : ( = ({ } }} /> - + = ({ onLayout={(sizes) => { // Persist only user drag updates so panel open/close and // programmatic restores do not overwrite the user's preference. + // sizes[1] is content-relative (react-resizable-panels sizes + // panels relative to their own PanelGroup) — convert back to + // viewport-relative before persisting, matching the frame + // sessionPanelSize is stored in. if ( effectiveSelectedSessionId && rightPanelResizeDraggingRef.current && sizes.length === 2 ) { + const viewportRelativeSize = toViewportRelativePercent( + sizes[1], + contentPanelWidthPercent + ); setSessionPanelSize( - clampPercent(sizes[1], sessionPanelMinSize, SESSION_PANEL_MAX_SIZE_PERCENT) + clampPercent( + viewportRelativeSize, + sessionPanelMinSize, + SESSION_PANEL_MAX_SIZE_PERCENT + ) ); } }} @@ -1320,8 +1418,10 @@ export const App: React.FC = ({
{isHomeSurface ? ( @@ -1405,9 +1505,9 @@ export const App: React.FC = ({ id="session-panel" order={2} ref={sessionPanelRef} - defaultSize={effectiveSessionPanelSize} - minSize={sessionPanelMinSize} - maxSize={SESSION_PANEL_MAX_SIZE_PERCENT} + defaultSize={sessionPanelSizeWithinContent} + minSize={sessionPanelMinSizeWithinContent} + maxSize={sessionPanelMaxSizeWithinContent} > {effectiveSelectedSessionId ? ( = ({ - {currentBoard && ( - document.body} - > - - - )} {/* Invisible mount of antd Upload so its CSS-in-JS styles stay registered even after the SessionPanel (which contains FileUpload) diff --git a/apps/agor-ui/src/components/App/boardPanelActions.test.ts b/apps/agor-ui/src/components/App/boardPanelActions.test.ts index 64b5f9906..5af5431ab 100644 --- a/apps/agor-ui/src/components/App/boardPanelActions.test.ts +++ b/apps/agor-ui/src/components/App/boardPanelActions.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { getShowCommentsPanelState, getToggleBoardPanelState } from './boardPanelActions'; +import { + getSelectAssistantPanelTabState, + getShowCommentsPanelState, + getToggleBoardPanelState, +} from './boardPanelActions'; describe('board panel navbar actions', () => { it('opens a closed panel on the Comments tab from Show comments tab', () => { @@ -29,4 +33,15 @@ describe('board panel navbar actions', () => { activeTab: 'comments', }); }); + + it.each([ + 'assistant', + 'all-sessions', + 'comments', + ] as const)('opens a closed panel on the %s tab when its rail button is clicked', (tab) => { + expect(getSelectAssistantPanelTabState(tab)).toEqual({ + collapsed: false, + activeTab: tab, + }); + }); }); diff --git a/apps/agor-ui/src/components/App/boardPanelActions.ts b/apps/agor-ui/src/components/App/boardPanelActions.ts index 6efd0a387..622eacf0f 100644 --- a/apps/agor-ui/src/components/App/boardPanelActions.ts +++ b/apps/agor-ui/src/components/App/boardPanelActions.ts @@ -24,3 +24,12 @@ export const getToggleBoardPanelState = (state: BoardLeftPanelState): BoardLeftP collapsed: true, }; }; + +// Used by every AssistantPanelRail button when the panel is collapsed: +// expand onto whichever tab was clicked. +export const getSelectAssistantPanelTabState = ( + tab: BoardAssistantPanelTab +): BoardLeftPanelState => ({ + collapsed: false, + activeTab: tab, +}); diff --git a/apps/agor-ui/src/components/App/panelSizing.test.ts b/apps/agor-ui/src/components/App/panelSizing.test.ts new file mode 100644 index 000000000..c1ba043b0 --- /dev/null +++ b/apps/agor-ui/src/components/App/panelSizing.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from 'vitest'; +import { + capSessionSizeForCanvasMin, + getContentPanelWidthPercent, + toContentRelativePercent, + toViewportRelativePercent, +} from './panelSizing'; + +describe('getContentPanelWidthPercent', () => { + it('subtracts the rail width from the viewport when collapsed', () => { + expect(getContentPanelWidthPercent(true, 4, 24)).toBe(96); + }); + + it('subtracts the expanded left panel width when open', () => { + expect(getContentPanelWidthPercent(false, 4, 24)).toBe(76); + }); +}); + +describe('viewport <-> content relative percent conversion', () => { + it('converts a viewport-relative size into a larger content-relative size when the content panel is narrower than the viewport', () => { + // Session panel wants 30% of the viewport; content panel is only 76% + // of the viewport (left panel expanded to 24%) — so within its own + // parent, the session panel must claim a bigger share. + expect(toContentRelativePercent(30, 76)).toBeCloseTo(39.4736, 3); + }); + + it('round-trips back to the original viewport-relative percent', () => { + const contentPanelWidthPercent = 76; + const viewportRelativePercent = 30; + const contentRelativePercent = toContentRelativePercent( + viewportRelativePercent, + contentPanelWidthPercent + ); + expect(toViewportRelativePercent(contentRelativePercent, contentPanelWidthPercent)).toBeCloseTo( + viewportRelativePercent, + 5 + ); + }); + + it('keeps the absolute (viewport-relative) session panel size constant when the left panel collapses to a rail', () => { + // Left panel expanded (24% of viewport) vs collapsed to a 4%-wide rail. + // A session panel pinned at 30% of the viewport should convert to two + // different content-relative percentages that both resolve back to the + // same 30% of the viewport — i.e. the same absolute pixel width. + const viewportRelativePercent = 30; + const expandedContentWidth = getContentPanelWidthPercent(false, 4, 24); + const collapsedContentWidth = getContentPanelWidthPercent(true, 4, 24); + + const expandedContentRelative = toContentRelativePercent( + viewportRelativePercent, + expandedContentWidth + ); + const collapsedContentRelative = toContentRelativePercent( + viewportRelativePercent, + collapsedContentWidth + ); + + expect(toViewportRelativePercent(expandedContentRelative, expandedContentWidth)).toBeCloseTo( + viewportRelativePercent, + 5 + ); + expect(toViewportRelativePercent(collapsedContentRelative, collapsedContentWidth)).toBeCloseTo( + viewportRelativePercent, + 5 + ); + }); + + it('returns 0 instead of NaN/Infinity when the content panel has no width', () => { + expect(toContentRelativePercent(30, 0)).toBe(0); + }); + + it('clamps content-relative percent to [0, 100]', () => { + expect(toContentRelativePercent(90, 10)).toBe(100); + }); + + it('clamps viewport-relative percent to [0, 100]', () => { + expect(toViewportRelativePercent(150, 90)).toBe(100); + }); +}); + +describe('capSessionSizeForCanvasMin', () => { + it('caps the session panel so the canvas panel keeps its minSize when the left panel is expanded', () => { + // Chat pinned at 75% of the viewport (its max); left panel expanded to + // 24%, leaving a 76%-wide content panel — converting to content-relative + // asks for ~98.7%, which would starve the canvas below its 20% minimum. + const contentPanelWidthPercent = getContentPanelWidthPercent(false, 4, 24); + const sessionContentRelativePercent = toContentRelativePercent(75, contentPanelWidthPercent); + + expect(capSessionSizeForCanvasMin(sessionContentRelativePercent, 20)).toBeLessThanOrEqual(80); + }); + + it('passes the size through unchanged when it already leaves the canvas its minimum', () => { + expect(capSessionSizeForCanvasMin(50, 20)).toBe(50); + }); +}); diff --git a/apps/agor-ui/src/components/App/panelSizing.ts b/apps/agor-ui/src/components/App/panelSizing.ts new file mode 100644 index 000000000..06e45c99d --- /dev/null +++ b/apps/agor-ui/src/components/App/panelSizing.ts @@ -0,0 +1,44 @@ +// Pure width math for the resizable App layout panels. Extracted so it's +// unit-testable without mounting the full App component tree. + +const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); + +// Width of the middle "content" panel (canvas + session panel) as a +// percentage of the full viewport — whatever's left once the left +// assistant/comments panel (rail or fully expanded) takes its share. +export const getContentPanelWidthPercent = ( + leftPanelCollapsed: boolean, + leftPanelCollapsedSize: number, + leftPanelExpandedSize: number +) => 100 - (leftPanelCollapsed ? leftPanelCollapsedSize : leftPanelExpandedSize); + +// The session/chat panel's persisted size is expressed as a percentage of +// the full viewport (so its absolute pixel width doesn't change when the +// left panel toggles between rail and expanded), but react-resizable-panels +// needs sizes expressed relative to each panel's own immediate parent — the +// content panel. These two functions convert between the two frames. Both +// clamp to [0, 100] and guard the divide so a momentarily-zero content width +// doesn't produce NaN/Infinity. +export const toContentRelativePercent = ( + viewportRelativePercent: number, + contentPanelWidthPercent: number +) => + contentPanelWidthPercent > 0 + ? clamp((viewportRelativePercent / contentPanelWidthPercent) * 100, 0, 100) + : 0; + +export const toViewportRelativePercent = ( + contentRelativePercent: number, + contentPanelWidthPercent: number +) => clamp((contentRelativePercent * contentPanelWidthPercent) / 100, 0, 100); + +// The session panel shares the content panel with the canvas, which enforces +// its own `minSize`. A content-relative session size only bounded to [0, 100] +// can ask for more room than leaves the canvas its minimum; react-resizable- +// panels then clamps the layout and the session panel visibly jumps when the +// left panel toggles. Cap the session panel's content-relative percentage so +// the canvas always keeps at least `canvasMinPercent`. +export const capSessionSizeForCanvasMin = ( + sessionContentRelativePercent: number, + canvasMinPercent: number +) => Math.min(sessionContentRelativePercent, 100 - canvasMinPercent); diff --git a/apps/agor-ui/src/components/BoardAssistantPanel/AssistantPanelRail.test.tsx b/apps/agor-ui/src/components/BoardAssistantPanel/AssistantPanelRail.test.tsx new file mode 100644 index 000000000..a8aeb9ecb --- /dev/null +++ b/apps/agor-ui/src/components/BoardAssistantPanel/AssistantPanelRail.test.tsx @@ -0,0 +1,52 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { AssistantPanelRail } from './AssistantPanelRail'; + +describe('AssistantPanelRail', () => { + it('renders one button per tab, fully visible (no clipped floating knob)', () => { + render(); + + expect(screen.getByRole('button', { name: 'Assistant' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Sessions' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Comments' })).toBeInTheDocument(); + }); + + it('opens the Assistant tab when its button is clicked', () => { + const onSelectTab = vi.fn(); + render(); + + fireEvent.click(screen.getByRole('button', { name: 'Assistant' })); + + expect(onSelectTab).toHaveBeenCalledExactlyOnceWith('assistant'); + }); + + it('opens the All sessions tab when its button is clicked', () => { + const onSelectTab = vi.fn(); + render(); + + fireEvent.click(screen.getByRole('button', { name: 'Sessions' })); + + expect(onSelectTab).toHaveBeenCalledExactlyOnceWith('all-sessions'); + }); + + it('opens the Comments tab when its button is clicked', () => { + const onSelectTab = vi.fn(); + render(); + + fireEvent.click(screen.getByRole('button', { name: 'Comments' })); + + expect(onSelectTab).toHaveBeenCalledExactlyOnceWith('comments'); + }); + + it('shows no unread badge on Comments when the count is zero', () => { + render(); + + expect(screen.queryByText('0')).not.toBeInTheDocument(); + }); + + it('shows the unread badge on Comments when there are unread comments', () => { + render(); + + expect(screen.getByText('3')).toBeInTheDocument(); + }); +}); diff --git a/apps/agor-ui/src/components/BoardAssistantPanel/AssistantPanelRail.tsx b/apps/agor-ui/src/components/BoardAssistantPanel/AssistantPanelRail.tsx new file mode 100644 index 000000000..81e8a4876 --- /dev/null +++ b/apps/agor-ui/src/components/BoardAssistantPanel/AssistantPanelRail.tsx @@ -0,0 +1,103 @@ +import { CommentOutlined, RobotOutlined, UnorderedListOutlined } from '@ant-design/icons'; +import { Badge, theme } from 'antd'; +import type React from 'react'; +import { memo } from 'react'; +import type { BoardAssistantPanelTab } from './BoardAssistantPanel'; + +interface RailItem { + key: BoardAssistantPanelTab; + label: string; + icon: React.ReactNode; +} + +const RAIL_ITEMS: RailItem[] = [ + { key: 'assistant', label: 'Assistant', icon: }, + { key: 'all-sessions', label: 'Sessions', icon: }, + { key: 'comments', label: 'Comments', icon: }, +]; + +export interface AssistantPanelRailProps { + onSelectTab: (tab: BoardAssistantPanelTab) => void; + unreadCommentsCount?: number; + hasUserMentions?: boolean; +} + +// Collapsed-state replacement for the old floating reopen knob (issue #123): +// a persistent, always-fully-visible icon rail rather than a half-clipped, +// low-contrast circle floating at the panel edge. +const AssistantPanelRailComponent: React.FC = ({ + onSelectTab, + unreadCommentsCount = 0, + hasUserMentions = false, +}) => { + const { token } = theme.useToken(); + + return ( +
+ {RAIL_ITEMS.map((item) => { + const button = ( + + ); + + if (item.key !== 'comments') return button; + + return ( + + {button} + + ); + })} +
+ ); +}; + +export const AssistantPanelRail = memo(AssistantPanelRailComponent); + +export default AssistantPanelRail; diff --git a/apps/agor-ui/src/components/BoardAssistantPanel/index.ts b/apps/agor-ui/src/components/BoardAssistantPanel/index.ts index 2b113e52a..5aabd9f65 100644 --- a/apps/agor-ui/src/components/BoardAssistantPanel/index.ts +++ b/apps/agor-ui/src/components/BoardAssistantPanel/index.ts @@ -1,2 +1,3 @@ +export { AssistantPanelRail } from './AssistantPanelRail'; export type { BoardAssistantPanelTab } from './BoardAssistantPanel'; export { BoardAssistantPanel, default } from './BoardAssistantPanel';