Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/layout-mode-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@truefoundry/trueforge-ui': patch
---

Bring Agents, Sessions, and Schedules chrome to drawer/dock/widget, keep overlay back/close controls (including widget Close), reopen compact Agent Config after close, and stack sessions list/detail on compact and mobile widths.
5 changes: 5 additions & 0 deletions .changeset/withrouter-default-true.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@truefoundry/trueforge-ui': patch
---

Default `TrueForgeUI` `withRouter` to `true`; pass `withRouter={false}` for embeds that must not own the URL.
26 changes: 14 additions & 12 deletions packages/trueforge-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,12 @@ show the title text (see [Custom layouts](#custom-layouts)).

`agentConfig` controls library chrome, draft composer, and how New Chat / Clear Chat behave.

| Mode | Layout chrome | Agent selection / New Chat |
| -------------------------------------- | ------------------------------- | --------------------------------------------------------- |
| `AgentLibraryWithComposer` _(default)_ | Agents + draft builder | New Chat opens draft; library picks a named agent |
| `SingleAgent` | Named-only, plain composer | Locked to `name`; New Chat / Clear Chat = new thread |
| `AgentLibrary` | Agents only (no draft) | Empty until pick; no New Chat; Clear Chat after selection |
| `AgentComposer` | Draft builder only (no library) | Always draft; New Chat / Clear Chat = fresh draft |
| Mode | Layout chrome | Agent selection / New Chat |
| -------------------------------------- | ------------------------------- | ------------------------------------------------------------------------ |
| `AgentLibraryWithComposer` _(default)_ | Agents + draft builder | New Chat opens draft; library picks a named agent |
| `SingleAgent` | Named-only, plain composer | Locked to `name`; New Chat / Clear Chat = new thread |
| `AgentLibrary` | Agents only (no draft) | Opens Agents Library by default; no New Chat; Clear Chat after selection |
| `AgentComposer` | Draft builder only (no library) | Always draft; New Chat / Clear Chat = fresh draft |

In library modes, picking an agent from Agents switches to a named chat for that agent **and remounts the runtime** so the new agent starts from a clean conversation. Draft chats can be promoted via **Save agent** (`server.saveAgent` on the resolved `AgentUIServer`). **Clear Chat** (thread header) resets the current named or draft session.

Expand Down Expand Up @@ -446,12 +446,14 @@ Mutable composers expose **Agent Config** for live model parameters, instruction

Built-in `layout` values:

| Value | Description |
| --------- | ---------------------------------------------------- |
| `sidebar` | Icon rail + recent session history + active thread |
| `drawer` | Full-bleed thread; sessions open in a slide-over |
| `dock` | Fixed-width right panel; list XOR thread stack |
| `widget` | Same stack as `dock`, opened from a bottom-right FAB |
| Value | Description |
| --------- | -------------------------------------------------------------------------------------------------- |
| `sidebar` | Icon rail (New Chat / Build Agent / Agents / Sessions / Schedules) + recent chats + active thread |
| `drawer` | Full-bleed thread; Recents top tab opens Chat History on the right; overlays replace the main pane |
| `dock` | Fixed-width right panel; thread stack with toolbar nav and overlay back navigation |
| `widget` | Same stack as `dock`, opened from a bottom-right FAB; Close stays available on overlays |

In every layout, Agents / Sessions / Schedules entry points appear when the host `agentConfig` mode and server ports enable them. Compact and mobile sessions use list → detail stack navigation; desktop sidebar/drawer keep the resizable split.

---

Expand Down
13 changes: 8 additions & 5 deletions packages/trueforge-ui/docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,16 @@ Public override surface (primitives stay theme/CSS — not slots):

## URL routing (`withRouter`)

Opt in to browser-URL sync for shell navigation. Requires `react-router-dom`
(v6 or v7) in the host; it is an optional peer and stays out of the bundle
unless `withRouter` is set, so dock/widget embeds and hosts that own their own
router should leave it off (the default).
Browser-URL sync for shell navigation is on by default. Requires `react-router-dom`
(v6 or v7) in the host; it is an optional peer. Pass `withRouter={false}` for
dock/widget embeds and hosts that own their own router (keeps `react-router` out
of the bundle for that mount).

```tsx
<TrueForgeUI server={server} layout="sidebar" withRouter />
<TrueForgeUI server={server} layout="sidebar" />;
{
/* or explicitly: withRouter={false} for embeds */
}
```

Places mirrored to the URL:
Expand Down
35 changes: 31 additions & 4 deletions packages/trueforge-ui/src/atoms/AgentsLibraryButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,25 @@ import { SEARCH_AGENTS_PAGE_SIZE } from './lib/useSearchAgentsList.js';

export type AgentsLibraryButtonProps = {
className?: string;
/** Sidebar rail: icon + label stacked. */
compact?: boolean;
/** Header/footer chrome: icon-only control. */
toolbar?: boolean;
};

export function AgentsLibraryButton({ className, compact = false }: AgentsLibraryButtonProps) {
export function AgentsLibraryButton({ className, compact = false, toolbar = false }: AgentsLibraryButtonProps) {
const server = useOptionalServer();
const shell = useOptionalShellMode();
const [countLabel, setCountLabel] = useState<string | null>(null);

const enabled = shell?.isLibraryEnabled === true && server != null;
const libraryOpen = shell?.libraryOpen === true;
const agentsListEpoch = shell?.agentsListEpoch ?? 0;
const skipCount = compact || toolbar;

useEffect(() => {
// Compact rail has no count badge; skip the catalog request.
if (!enabled || !server || compact) return;
// Rail / toolbar have no count badge; skip the catalog request.
if (!enabled || !server || skipCount) return;
let cancelled = false;
void server
.searchAgents({ limit: SEARCH_AGENTS_PAGE_SIZE })
Expand All @@ -38,10 +42,33 @@ export function AgentsLibraryButton({ className, compact = false }: AgentsLibrar
return () => {
cancelled = true;
};
}, [enabled, server, agentsListEpoch, compact]);
}, [enabled, server, agentsListEpoch, skipCount]);

if (!enabled) return null;

if (toolbar) {
return (
<button
type="button"
aria-label="Agents"
title="Agents"
aria-current={libraryOpen ? 'page' : undefined}
className={auiButtonClass({
variant: 'ghost',
size: 'icon',
className: cn(
libraryOpen &&
'bg-primary-button-bg font-medium text-primary-button-text hover:bg-primary-button-hover hover:text-primary-button-text',
className,
),
})}
onClick={() => shell?.setLibraryOpen(true)}
>
<Icon name="library-big" />
</button>
);
}

return (
<div className={cn('relative min-w-0', compact ? 'flex justify-center' : 'w-full', className)}>
<button
Expand Down
28 changes: 27 additions & 1 deletion packages/trueforge-ui/src/atoms/SchedulesButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import { cn } from './lib/cn.js';

export type SchedulesButtonProps = {
className?: string;
/** Sidebar rail: icon + label stacked. */
compact?: boolean;
/** Header/footer chrome: icon-only control. */
toolbar?: boolean;
};

export function SchedulesButton({ className, compact = false }: SchedulesButtonProps) {
export function SchedulesButton({ className, compact = false, toolbar = false }: SchedulesButtonProps) {
const shell = useOptionalShellMode();
const scheduleServer = useOptionalScheduleServer();

Expand All @@ -21,6 +24,29 @@ export function SchedulesButton({ className, compact = false }: SchedulesButtonP

if (!enabled) return null;

if (toolbar) {
return (
<button
type="button"
aria-label="Schedules"
title="Schedules"
aria-current={open ? 'page' : undefined}
className={auiButtonClass({
variant: 'ghost',
size: 'icon',
className: cn(
open &&
'bg-primary-button-bg font-medium text-primary-button-text hover:bg-primary-button-hover hover:text-primary-button-text',
className,
),
})}
onClick={() => shell.setSchedulesOpen(!open)}
>
<Icon name="calendar-clock" />
</button>
);
}

return (
<div className={cn('relative min-w-0', compact ? 'flex justify-center' : 'w-full', className)}>
<button
Expand Down
54 changes: 41 additions & 13 deletions packages/trueforge-ui/src/atoms/SessionsBrowserButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ import { cn } from './lib/cn.js';

export type SessionsBrowserButtonProps = {
className?: string;
/** Sidebar rail: icon + label stacked. */
compact?: boolean;
/** Header/footer chrome: icon-only control. */
toolbar?: boolean;
};

export function SessionsBrowserButton({ className, compact = false }: SessionsBrowserButtonProps) {
export function SessionsBrowserButton({ className, compact = false, toolbar = false }: SessionsBrowserButtonProps) {
const sessionsServer = useOptionalAgentSessionsServer();
const shell = useOptionalShellMode();
const { updateShareSearch } = useSessionShareSearch();
Expand All @@ -23,6 +26,42 @@ export function SessionsBrowserButton({ className, compact = false }: SessionsBr

if (!enabled) return null;

const openSessions = () => {
if (!sessionsOpen) {
const share = readSessionShareSearch(window.location.search);
updateShareSearch({
view: 'sessions',
agentId: null,
sessionId: null,
timeRange: share.timeRange ?? defaultSessionTimeRange(),
});
}
shell.setSessionsOpen(true);
};

if (toolbar) {
return (
<button
type="button"
aria-label="Sessions"
title="Sessions"
aria-current={sessionsOpen ? 'page' : undefined}
className={auiButtonClass({
variant: 'ghost',
size: 'icon',
className: cn(
sessionsOpen &&
'bg-primary-button-bg font-medium text-primary-button-text hover:bg-primary-button-hover hover:text-primary-button-text',
className,
),
})}
onClick={openSessions}
>
<Icon name="message-square-text" />
</button>
);
}

return (
<div className={cn('relative min-w-0', compact ? 'flex justify-center' : 'w-full', className)}>
<button
Expand All @@ -40,18 +79,7 @@ export function SessionsBrowserButton({ className, compact = false }: SessionsBr
'bg-primary-button-bg font-medium text-primary-button-text hover:bg-primary-button-hover hover:text-primary-button-text',
),
})}
onClick={() => {
if (!sessionsOpen) {
const share = readSessionShareSearch(window.location.search);
updateShareSearch({
view: 'sessions',
agentId: null,
sessionId: null,
timeRange: share.timeRange ?? defaultSessionTimeRange(),
});
}
shell.setSessionsOpen(true);
}}
onClick={openSessions}
>
<Icon name="message-square-text" size={compact ? 14 : undefined} />
{compact ? (
Expand Down
9 changes: 6 additions & 3 deletions packages/trueforge-ui/src/atoms/UserAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { cn } from './lib/cn.js';
import { Avatar, AvatarFallback } from './primitives/Avatar.js';

export type UserAvatarProps = {
labeled?: boolean; // Uses the sidebar rail width instead of the compact chrome width
/** Sidebar rail: wider control with display name under the avatar. */
labeled?: boolean;
className?: string;
};

Expand All @@ -29,14 +30,16 @@ export function UserAvatar({ labeled = false, className }: UserAvatarProps) {
title={displayName}
className={cn(
'flex min-w-0 shrink-0 flex-col items-center justify-center gap-0.5 text-text-secondary',
labeled ? 'w-14.5' : 'max-w-20',
labeled ? 'w-14.5' : undefined,
className,
)}
>
<Avatar size="sm">
<AvatarFallback>{getUserInitials(displayName)}</AvatarFallback>
</Avatar>
<span className="w-full truncate text-center text-[0.625rem] leading-tight">{displayName}</span>
{labeled ? (
<span className="w-full truncate text-center text-[0.625rem] leading-tight">{displayName}</span>
) : null}
</div>
);
}
Expand Down
Loading
Loading