+ options={[
+ { value: '', label: 'All' },
+ ...agents.map(agent => ({ value: agent.agentId ?? agent.name, label: agent.name })),
+ ]}
+ onValueChange={value => onAgentChange(value.length === 0 ? null : value)}
+ />
+
{menuOpen ? (
-
+
{customPickerOpen ? (
Select Time Range
@@ -151,7 +151,7 @@ export function AgentSessionsFilters({
setFromValue(event.target.value)}
/>
@@ -161,7 +161,7 @@ export function AgentSessionsFilters({
setToValue(event.target.value)}
/>
@@ -169,52 +169,56 @@ export function AgentSessionsFilters({
Timezone: {formatTimezoneOffsetLabel()}
-
) : null}
-
+
setCustomPickerOpen(true)}
>
- Custom Time Range
-
- {SESSION_TIME_PRESETS.map(preset => (
- Custom Time Range
+ {
- const endTs = Date.now();
- onTimeRangeChange({
- startTs: endTs - preset.windowMs,
- endTs,
- timeWindowMs: preset.windowMs,
- });
- setMenuOpen(false);
- setCustomPickerOpen(false);
- }}
- >
- {preset.label}
-
- ))}
+ />
+
+ {SESSION_TIME_PRESETS.map(preset => {
+ const selected = timeRange.timeWindowMs === preset.windowMs;
+ return (
+ {
+ const endTs = Date.now();
+ onTimeRangeChange({
+ startTs: endTs - preset.windowMs,
+ endTs,
+ timeWindowMs: preset.windowMs,
+ });
+ setMenuOpen(false);
+ setCustomPickerOpen(false);
+ }}
+ >
+ {preset.label}
+
+
+ );
+ })}
) : null}
diff --git a/packages/trueforge-ui/src/atoms/lib/selectClasses.ts b/packages/trueforge-ui/src/atoms/lib/selectClasses.ts
new file mode 100644
index 000000000..65ae5f88c
--- /dev/null
+++ b/packages/trueforge-ui/src/atoms/lib/selectClasses.ts
@@ -0,0 +1,27 @@
+import { cn } from './cn.js';
+import { auiInputClass } from './inputClasses.js';
+
+/**
+ * Shared dropdown chrome. Every select-like surface (PopoverSelect, the session
+ * time-range popover) composes these so triggers, menus, and option rows stay
+ * visually identical; callers pass `className` for width and placement only.
+ */
+export function auiSelectTriggerClass(className?: string): string {
+ return auiInputClass(cn('flex h-9 cursor-pointer items-center justify-between gap-2 pr-2 text-left', className));
+}
+
+export function auiSelectMenuClass(className?: string): string {
+ return cn(
+ 'bg-card-bg text-text-primary absolute top-full z-50 mt-1 max-h-64 overflow-y-auto rounded-md border border-border p-1 shadow-md',
+ className,
+ );
+}
+
+export function auiSelectOptionClass(className?: string): string {
+ return cn(
+ 'text-text-primary flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-none transition-colors',
+ 'hover:bg-ghost-button-hover focus:bg-dropdown-selected-item-bg focus:text-dropdown-selected-item-text',
+ 'disabled:pointer-events-none disabled:opacity-50',
+ className,
+ );
+}
diff --git a/packages/trueforge-ui/src/atoms/primitives/DropdownMenu.tsx b/packages/trueforge-ui/src/atoms/primitives/DropdownMenu.tsx
index 5a77fb39f..c0fe1e97d 100644
--- a/packages/trueforge-ui/src/atoms/primitives/DropdownMenu.tsx
+++ b/packages/trueforge-ui/src/atoms/primitives/DropdownMenu.tsx
@@ -1,7 +1,20 @@
-import React, { useEffect, useId, useRef, useState } from 'react';
+'use client';
+
+import React, { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
+import { createPortal } from 'react-dom';
import { cn } from '../lib/cn.js';
+/** Keep portaled chrome under ThemeProvider so preset/custom CSS vars still apply. */
+function themePortalRoot(from: HTMLElement | null): HTMLElement {
+ // A native
}
>
diff --git a/packages/trueforge-ui/src/icons/IconRegistry.tsx b/packages/trueforge-ui/src/icons/IconRegistry.tsx
index 923f97853..3ec1ed2c1 100644
--- a/packages/trueforge-ui/src/icons/IconRegistry.tsx
+++ b/packages/trueforge-ui/src/icons/IconRegistry.tsx
@@ -7,6 +7,8 @@ import {
Bot,
Box,
Brain,
+ Calendar,
+ CalendarPlus,
Check,
ChevronDown,
ChevronLeft,
@@ -40,6 +42,7 @@ import {
PanelLeftClose,
PanelLeftOpen,
Paperclip,
+ Pause,
Pencil,
Play,
Plug,
@@ -115,6 +118,9 @@ const defaults: Record
= {
check: Check,
pencil: Pencil,
play: Play,
+ pause: Pause,
+ calendar: Calendar,
+ 'calendar-plus': CalendarPlus,
'square-pen': SquarePen,
'rotate-right': RotateCw,
broom: Broom,
diff --git a/packages/trueforge-ui/src/index.ts b/packages/trueforge-ui/src/index.ts
index 54b0f732c..061e635b4 100644
--- a/packages/trueforge-ui/src/index.ts
+++ b/packages/trueforge-ui/src/index.ts
@@ -36,8 +36,40 @@ export type { ButtonProps, ButtonSize, ButtonVariant } from './atoms/primitives/
export { CenteredModal } from './atoms/primitives/CenteredModal.js';
export type { CenteredModalProps } from './atoms/primitives/CenteredModal.js';
export type { IconButtonProps } from './atoms/primitives/IconButton.js';
+export { PopoverSelect } from './atoms/primitives/PopoverSelect.js';
+export type { PopoverSelectOption, PopoverSelectProps } from './atoms/primitives/PopoverSelect.js';
+export { SideDrawer } from './atoms/primitives/SideDrawer.js';
+export type { SideDrawerAnchor, SideDrawerProps, SideDrawerSize } from './atoms/primitives/SideDrawer.js';
export { Switch } from './atoms/primitives/Switch.js';
export type { SwitchProps, SwitchSize } from './atoms/primitives/Switch.js';
+export {
+ DEFAULT_TABLE_PAGE_SIZE,
+ TABLE_PAGE_SIZE_OPTIONS,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TablePagination,
+ TableRow,
+} from './atoms/primitives/Table.js';
+export type {
+ TableBodyProps,
+ TableCellProps,
+ TableHeadProps,
+ TableHeaderProps,
+ TablePaginationProps,
+ TableProps,
+ TableRowProps,
+} from './atoms/primitives/Table.js';
+export { ScheduleFormDrawer } from './atoms/schedules/ScheduleFormDrawer.js';
+export type { ScheduleFormDrawerProps } from './atoms/schedules/ScheduleFormDrawer.js';
+export { ScheduleFormFields } from './atoms/schedules/ScheduleFormFields.js';
+export type { ScheduleFormFieldsProps } from './atoms/schedules/ScheduleFormFields.js';
+export { SchedulesPage } from './atoms/schedules/SchedulesPage.js';
+export { ScheduleStatusBadge } from './atoms/schedules/ScheduleStatusBadge.js';
+export { SchedulesButton } from './atoms/SchedulesButton.js';
+export type { SchedulesButtonProps } from './atoms/SchedulesButton.js';
export { Icon } from './icons/Icon.js';
export type { IconProps } from './icons/Icon.js';
@@ -243,7 +275,9 @@ export {
useCatalogServer,
useOptionalAgentSessionsServer,
useOptionalCatalogServer,
+ useOptionalScheduleServer,
useOptionalServer,
+ useScheduleServer,
useServer,
useServerCapabilities,
} from './server/ServerContext.js';
diff --git a/packages/trueforge-ui/src/layouts/DrawerLayout.tsx b/packages/trueforge-ui/src/layouts/DrawerLayout.tsx
index a2645f385..82bb80fab 100644
--- a/packages/trueforge-ui/src/layouts/DrawerLayout.tsx
+++ b/packages/trueforge-ui/src/layouts/DrawerLayout.tsx
@@ -15,6 +15,9 @@ import { useOptionalShellMode } from '../server/ShellModeContext.js';
import { useSlot } from '../theme/SlotsProvider.js';
const TruefoundrySettingsBuilder = lazy(() => import('../containers/SettingsBuilder/index.js'));
+const SchedulesPage = lazy(() =>
+ import('../atoms/schedules/SchedulesPage.js').then(m => ({ default: m.SchedulesPage })),
+);
export function DrawerLayout({ className }: { className?: string }) {
const aui = useAui();
@@ -34,7 +37,8 @@ export function DrawerLayout({ className }: { className?: string }) {
const settingsOpen = shell?.settingsOpen === true;
const libraryOpen = shell?.libraryOpen === true;
const sessionsOpen = shell?.sessionsOpen === true;
- const overlayOpen = settingsOpen || libraryOpen || sessionsOpen;
+ const schedulesOpen = shell?.schedulesOpen === true;
+ const overlayOpen = settingsOpen || libraryOpen || sessionsOpen || schedulesOpen;
useEffect(() => {
if (!threadsOpen) return;
@@ -67,6 +71,7 @@ export function DrawerLayout({ className }: { className?: string }) {
shell.openDraft();
} else {
shell?.setSettingsOpen(false);
+ shell?.setSchedulesOpen(false);
aui.threads().switchToNewThread();
}
setThreadsOpen(false);
@@ -136,6 +141,22 @@ export function DrawerLayout({ className }: { className?: string }) {
) : libraryOpen ? (
setThreadsOpen(false)} />
+ ) : schedulesOpen ? (
+
+
+ Loading
+
+ }
+ >
+
+
) : isIdle ? (
) : (
diff --git a/packages/trueforge-ui/src/layouts/SidebarLayout.tsx b/packages/trueforge-ui/src/layouts/SidebarLayout.tsx
index 672f912c2..15984b2e1 100644
--- a/packages/trueforge-ui/src/layouts/SidebarLayout.tsx
+++ b/packages/trueforge-ui/src/layouts/SidebarLayout.tsx
@@ -18,6 +18,9 @@ import { useSlot } from '../theme/SlotsProvider.js';
import { useBrand } from '../theme/ThemeProvider.js';
const TruefoundrySettingsBuilder = lazy(() => import('../containers/SettingsBuilder/index.js'));
+const SchedulesPage = lazy(() =>
+ import('../atoms/schedules/SchedulesPage.js').then(m => ({ default: m.SchedulesPage })),
+);
// Survives ChatProvider remounts when openDraft / selectAgent bumps runtimeKey.
let desktopCollapsed = false;
@@ -36,6 +39,7 @@ export function SidebarLayout({ className }: { className?: string }) {
const AgentsLibraryButton = useSlot('AgentsLibraryButton');
const SessionsBrowserButton = useSlot('SessionsBrowserButton');
const SessionsPage = useSlot('SessionsPage');
+ const SchedulesButton = useSlot('SchedulesButton');
const ClearChatButton = useSlot('ClearChatButton');
const SaveAgentButton = useSlot('SaveAgentButton');
const SelectAgentEmptyState = useSlot('SelectAgentEmptyState');
@@ -56,7 +60,8 @@ export function SidebarLayout({ className }: { className?: string }) {
const settingsOpen = shell?.settingsOpen === true;
const libraryOpen = shell?.libraryOpen === true;
const sessionsOpen = shell?.sessionsOpen === true;
- const overlayOpen = settingsOpen || libraryOpen || sessionsOpen;
+ const schedulesOpen = shell?.schedulesOpen === true;
+ const overlayOpen = settingsOpen || libraryOpen || sessionsOpen || schedulesOpen;
const hasChatHeaderContent = useChatHeaderContentVisible();
useEffect(() => {
@@ -91,6 +96,7 @@ export function SidebarLayout({ className }: { className?: string }) {
return;
}
shell?.setSettingsOpen(false);
+ shell?.setSchedulesOpen(false);
void Promise.resolve(aui.threads().switchToNewThread()).catch(() => undefined);
};
@@ -142,6 +148,7 @@ export function SidebarLayout({ className }: { className?: string }) {
) : null}
+
@@ -216,6 +223,22 @@ export function SidebarLayout({ className }: { className?: string }) {
) : libraryOpen ? (
setMobileNavOpen(false)} />
+ ) : schedulesOpen ? (
+
+
+ Loading
+
+ }
+ >
+
+
) : isIdle ? (
) : (
diff --git a/packages/trueforge-ui/src/layouts/StackChatPanel.tsx b/packages/trueforge-ui/src/layouts/StackChatPanel.tsx
index e638633dd..58731628e 100644
--- a/packages/trueforge-ui/src/layouts/StackChatPanel.tsx
+++ b/packages/trueforge-ui/src/layouts/StackChatPanel.tsx
@@ -15,6 +15,9 @@ import { useOptionalShellMode } from '../server/ShellModeContext.js';
import { useSlot } from '../theme/SlotsProvider.js';
const TruefoundrySettingsBuilder = lazy(() => import('../containers/SettingsBuilder/index.js'));
+const SchedulesPage = lazy(() =>
+ import('../atoms/schedules/SchedulesPage.js').then(m => ({ default: m.SchedulesPage })),
+);
export type StackChatPanelProps = {
className?: string;
@@ -40,6 +43,7 @@ export function StackChatPanel({ className, threadHeaderEnd }: StackChatPanelPro
const settingsOpen = shell?.settingsOpen === true;
const libraryOpen = shell?.libraryOpen === true;
const sessionsOpen = shell?.sessionsOpen === true;
+ const schedulesOpen = shell?.schedulesOpen === true;
useEffect(() => {
if (isIdle) return;
@@ -78,6 +82,24 @@ export function StackChatPanel({ className, threadHeaderEnd }: StackChatPanelPro
+ ) : schedulesOpen ? (
+
+
+
+ Loading
+
+ }
+ >
+
+
+