@iloveagents/foundry-web-ui 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@ import { ComposerAddMenu } from "./composer-add-menu.js";
12
12
  import { ContextPins } from "./context-bar.js";
13
13
  import { ChatContext } from "./chat-context.js";
14
14
  import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
15
- import { ChatComposerActionsSlot, ChatComposerBannerSlot, useChatThreadSurface, useComposerActionsOwnSend, } from "./chat-slots.js";
15
+ import { ChatComposerActionsSlot, ChatComposerBannerSlot, ChatHomeStartersSlot, useChatThreadSurface, useComposerActionsOwnSend, } from "./chat-slots.js";
16
16
  import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
17
17
  import { ReasoningEffortPicker } from "./reasoning-effort-picker.js";
18
18
  import { ChatMessageParts } from "./chat-message-parts.js";
@@ -138,7 +138,7 @@ export function ChatContent({ centerComposer = false, composerIntro, composerFoo
138
138
  // `overflow-hidden`, not scroll: a surface owns its own layout,
139
139
  // and a full-bleed one (an avatar cropped to the frame) must be
140
140
  // able to reach the edges rather than sit in a scroll box.
141
- _jsx("div", { className: "absolute inset-0 z-10 overflow-hidden bg-background", children: _jsx(ThreadSurface, {}) })), _jsxs(ThreadPrimitive.Viewport, { className: cn("absolute inset-0 overflow-y-auto bg-background", ThreadSurface && "invisible"), "aria-hidden": ThreadSurface ? true : undefined, children: [_jsx(ThreadPrimitive.Empty, { children: emptyState ?? (_jsxs("div", { className: cn("flex min-h-full flex-col", "items-center justify-center", "px-4"), children: [_jsx(Bot, { className: "size-16 mb-6 text-primary/70" }), _jsx("h1", { className: "text-[2rem] font-normal text-foreground text-center mb-6", children: "How can I help you today?" }), starterSuggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-2 max-w-lg", children: starterSuggestions.map((prompt) => (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, asChild: true, children: _jsx(Button, { variant: "outline", className: "text-sm", children: prompt }) }, prompt))) }))] })) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "mx-auto max-w-3xl px-4 pt-8 pb-4", children: [_jsx(ThreadPrimitive.Messages, { components: {
141
+ _jsx("div", { className: "absolute inset-0 z-10 overflow-hidden bg-background", children: _jsx(ThreadSurface, {}) })), _jsxs(ThreadPrimitive.Viewport, { className: cn("absolute inset-0 overflow-y-auto bg-background", ThreadSurface && "invisible"), "aria-hidden": ThreadSurface ? true : undefined, children: [_jsx(ThreadPrimitive.Empty, { children: emptyState ?? (_jsxs("div", { className: cn("flex min-h-full flex-col", "items-center justify-center", "px-4"), children: [_jsx(Bot, { className: "size-16 mb-6 text-primary/70" }), _jsx("h1", { className: "text-[2rem] font-normal text-foreground text-center mb-6", children: "How can I help you today?" }), _jsx(ChatHomeStartersSlot, {}), starterSuggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-2 max-w-lg", children: starterSuggestions.map((prompt) => (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, asChild: true, children: _jsx(Button, { variant: "outline", className: "text-sm", children: prompt }) }, prompt))) }))] })) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "mx-auto max-w-3xl px-4 pt-8 pb-4", children: [_jsx(ThreadPrimitive.Messages, { components: {
142
142
  UserMessage,
143
143
  EditComposer,
144
144
  AssistantMessage,
@@ -12,7 +12,7 @@ import { ComposerContextBadges, SentContextBadges } from "./context-badges.js";
12
12
  import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
13
13
  import { ComposerAddMenu } from "./composer-add-menu.js";
14
14
  import { DEFAULT_COMPOSER_PLACEHOLDER } from "./assistant-chat.js";
15
- import { ChatComposerActionsSlot, ChatComposerBannerSlot, ChatLauncherBadgeSlot, useChatThreadSurface, useComposerActionsOwnSend, } from "./chat-slots.js";
15
+ import { ChatComposerActionsSlot, ChatComposerBannerSlot, ChatHomeStartersSlot, ChatLauncherBadgeSlot, useChatThreadSurface, useComposerActionsOwnSend, } from "./chat-slots.js";
16
16
  import { ChatMessageParts } from "./chat-message-parts.js";
17
17
  import { ReasoningEffortPicker } from "./reasoning-effort-picker.js";
18
18
  import { LoadingIndicator } from "./loading-indicator.js";
@@ -106,7 +106,7 @@ export function ChatBubble() {
106
106
  height: 64,
107
107
  } })), !isOpen && (_jsxs("div", { className: "fixed z-50 group", style: { left: CHAT_BUBBLE_INSET, bottom: CHAT_BUBBLE_INSET }, children: [_jsx("button", { type: "button", onClick: openBubble, className: cn("size-10 rounded-full", "bg-primary text-primary-foreground shadow-lg", "flex items-center justify-center", "hover:bg-primary/90 transition-all duration-200", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"), "aria-label": "Ask the AI", children: _jsx(Sparkles, { className: "size-4" }) }), _jsx(ChatLauncherBadgeSlot, {})] })), isOpen && (_jsxs("div", { ref: panelRef, "data-chat-surface": "bubble", className: cn("fixed z-50", "rounded-2xl border border-border", "bg-background shadow-2xl", "flex flex-col overflow-hidden", "w-95 h-150"), style: dragPos
108
108
  ? { left: dragPos.x, top: dragPos.y }
109
- : { left: CHAT_BUBBLE_INSET, bottom: CHAT_BUBBLE_INSET }, children: [_jsxs("div", { className: cn("flex items-center justify-between px-4 py-2.5 border-b border-border shrink-0", "cursor-grab active:cursor-grabbing select-none"), style: { touchAction: "none" }, onPointerDown: onHeaderPointerDown, onPointerMove: onHeaderPointerMove, onPointerUp: onHeaderPointerUp, onLostPointerCapture: onHeaderLostPointerCapture, children: [_jsxs("div", { className: "flex flex-col min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-sm", children: [_jsx(Sparkles, { className: "size-3.5 text-primary shrink-0" }), _jsx("span", { className: "font-semibold text-foreground truncate", children: pageLabel })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: threadActive ? "Active thread" : "New conversation" })] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [_jsx(TooltipIconButton, { tooltip: "New Thread", size: "icon", className: "size-7", onClick: startNewThread, children: _jsx(SquarePen, { className: "size-3.5" }) }), _jsx(TooltipIconButton, { tooltip: "Expand chat", size: "icon", className: "size-7", onClick: expandChat, children: _jsx(Maximize2, { className: "size-3.5" }) }), _jsx(TooltipIconButton, { tooltip: "Close", size: "icon", className: "size-7", onClick: handleClose, children: _jsx(X, { className: "size-4" }) })] })] }), _jsxs(ThreadPrimitive.Root, { className: "flex flex-col flex-1 overflow-hidden", children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [ThreadSurface && (_jsx("div", { className: "absolute inset-0 z-10 overflow-hidden bg-background", children: _jsx(ThreadSurface, {}) })), _jsxs(ThreadPrimitive.Viewport, { className: cn("absolute inset-0 overflow-y-auto", ThreadSurface && "invisible"), "aria-hidden": ThreadSurface ? true : undefined, children: [_jsx(ThreadPrimitive.Empty, { children: _jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Ask the agent about this page" }) }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "px-4 pt-4 pb-3", children: [_jsx(ThreadPrimitive.Messages, { components: {
109
+ : { left: CHAT_BUBBLE_INSET, bottom: CHAT_BUBBLE_INSET }, children: [_jsxs("div", { className: cn("flex items-center justify-between px-4 py-2.5 border-b border-border shrink-0", "cursor-grab active:cursor-grabbing select-none"), style: { touchAction: "none" }, onPointerDown: onHeaderPointerDown, onPointerMove: onHeaderPointerMove, onPointerUp: onHeaderPointerUp, onLostPointerCapture: onHeaderLostPointerCapture, children: [_jsxs("div", { className: "flex flex-col min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-sm", children: [_jsx(Sparkles, { className: "size-3.5 text-primary shrink-0" }), _jsx("span", { className: "font-semibold text-foreground truncate", children: pageLabel })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: threadActive ? "Active thread" : "New conversation" })] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [_jsx(TooltipIconButton, { tooltip: "New Thread", size: "icon", className: "size-7", onClick: startNewThread, children: _jsx(SquarePen, { className: "size-3.5" }) }), _jsx(TooltipIconButton, { tooltip: "Expand chat", size: "icon", className: "size-7", onClick: expandChat, children: _jsx(Maximize2, { className: "size-3.5" }) }), _jsx(TooltipIconButton, { tooltip: "Close", size: "icon", className: "size-7", onClick: handleClose, children: _jsx(X, { className: "size-4" }) })] })] }), _jsxs(ThreadPrimitive.Root, { className: "flex flex-col flex-1 overflow-hidden", children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [ThreadSurface && (_jsx("div", { className: "absolute inset-0 z-10 overflow-hidden bg-background", children: _jsx(ThreadSurface, {}) })), _jsxs(ThreadPrimitive.Viewport, { className: cn("absolute inset-0 overflow-y-auto", ThreadSurface && "invisible"), "aria-hidden": ThreadSurface ? true : undefined, children: [_jsx(ThreadPrimitive.Empty, { children: _jsxs("div", { className: "flex min-h-full flex-col items-center justify-center gap-4 p-4", children: [_jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Ask the agent about this page" }), _jsx(ChatHomeStartersSlot, {})] }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "px-4 pt-4 pb-3", children: [_jsx(ThreadPrimitive.Messages, { components: {
110
110
  UserMessage: BubbleUserMessage,
111
111
  AssistantMessage: BubbleAssistantMessage,
112
112
  } }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), _jsx(ThreadPrimitive.If, { empty: false, children: !ThreadSurface && _jsx(BubbleScrollToBottomButton, {}) })] }), _jsxs("div", { className: "shrink-0 border-t border-border", children: [_jsx(ChatComposerBannerSlot, {}), _jsxs("div", { className: "px-3 py-3", children: [_jsx(ChatContext, {}), _jsxs(ComposerPrimitive.Root, { className: cn(
@@ -27,7 +27,7 @@ export function ChatContext() {
27
27
  const events = ["focusin", "pointerup", "input", "selectionchange"];
28
28
  for (const event of events)
29
29
  document.addEventListener(event, refresh);
30
- const handles = "[data-focused], [data-context-ref-type='entity-field']";
30
+ const handles = "[data-focused], [data-context-ref-type='entity-field'], [data-agent-component]";
31
31
  const observer = typeof MutationObserver === "undefined"
32
32
  ? null
33
33
  : new MutationObserver((records) => {
@@ -42,6 +42,8 @@ export function ChatContext() {
42
42
  childList: true,
43
43
  attributes: true,
44
44
  attributeFilter: [
45
+ "data-agent-selected",
46
+ "data-agent-component-label",
45
47
  "data-focused",
46
48
  "data-focus-title",
47
49
  "data-focus-kind",
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from "react";
2
+ /** One compact conversation home for workbench, focus and embedded chat panes. */
3
+ export declare function ChatEmptyState({ pageLabel, children, showHomeStarters, homeStarterPriority, }: {
4
+ pageLabel?: string;
5
+ children?: ReactNode;
6
+ showHomeStarters?: boolean;
7
+ homeStarterPriority?: number;
8
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChatHomeStartersSlot } from "./chat-slots.js";
3
+ /** One compact conversation home for workbench, focus and embedded chat panes. */
4
+ export function ChatEmptyState({ pageLabel, children, showHomeStarters = true, homeStarterPriority = 0, }) {
5
+ return (_jsxs("div", { className: "flex min-h-full flex-col justify-start gap-3 px-6 pb-8 pt-0", children: [_jsx("h1", { className: "text-lg font-semibold tracking-tight", children: "What would you like to get done?" }), _jsx("p", { className: "max-w-sm text-sm leading-relaxed text-muted-foreground", children: pageLabel && !pageLabel.startsWith("/") ? (_jsxs(_Fragment, { children: ["Work with ", _jsx("span", { className: "font-medium text-foreground", children: pageLabel }), " or across your spaces: find information, create or edit content, and run actions."] })) : ("Find information, create or edit content, and run actions across your spaces.") }), _jsx("p", { className: "max-w-sm text-xs leading-relaxed text-muted-foreground", children: "Your conversation continues as you browse. Select text or a field to work more specifically." }), showHomeStarters && _jsx(ChatHomeStartersSlot, { priority: homeStarterPriority }), children] }));
6
+ }
@@ -10,6 +10,8 @@
10
10
  * shell's chat components render the slots wherever they belong.
11
11
  *
12
12
  * Render points:
13
+ * - ``homeStarters`` — contextual starters inside the empty chat home,
14
+ * alongside its introduction. The module owns discovery and execution.
13
15
  * - ``composerBanner`` — directly above the composer in BOTH the full
14
16
  * chat (``ChatContent``) and the floating bubble panel. It renders
15
17
  * inside a fixed (non-scrolling) footer, so it never scrolls away
@@ -40,8 +42,10 @@
40
42
  * Type one character and Send comes back. Opt-in, because a slot holding
41
43
  * something unrelated must not make Send disappear.
42
44
  */
43
- import type { ComponentType } from "react";
45
+ import { type ComponentType } from "react";
44
46
  interface ChatSlotsState {
47
+ /** Contextual conversation starters. Only mounted on the empty chat home. */
48
+ homeStarters: ComponentType | null;
45
49
  composerBanner: ComponentType | null;
46
50
  /** Extra capabilities in the context flyout. Providers own section headings
47
51
  * and can dismiss the flyout before opening a dialog or starting an action. */
@@ -64,6 +68,9 @@ export declare const useChatSlotsStore: import("zustand").UseBoundStore<import("
64
68
  */
65
69
  export declare function registerChatSlots(slots: Partial<ChatSlotsState>): void;
66
70
  export declare function ChatComposerBannerSlot(): import("react/jsx-runtime").JSX.Element | null;
71
+ export declare function ChatHomeStartersSlot({ priority }: {
72
+ priority?: number;
73
+ }): import("react/jsx-runtime").JSX.Element | null;
67
74
  export declare function ChatContextDetailsSlot({ onDismiss }: {
68
75
  onDismiss: () => void;
69
76
  }): import("react/jsx-runtime").JSX.Element | null;
@@ -1,7 +1,53 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Chat extension slots — module-injectable UI anchored to the chat chrome.
4
+ *
5
+ * Modules (via ``bootstrapShell``) sometimes need to render inside the
6
+ * chat surfaces the shell owns — e.g. a background-task progress strip
7
+ * pinned above the composer, or an activity badge on the collapsed chat
8
+ * launcher. Threading those through props would couple the shell to
9
+ * feature modules, so this is a tiny registry instead: a module calls
10
+ * :func:`registerChatSlots` once (import time or ``useInit``) and the
11
+ * shell's chat components render the slots wherever they belong.
12
+ *
13
+ * Render points:
14
+ * - ``homeStarters`` — contextual starters inside the empty chat home,
15
+ * alongside its introduction. The module owns discovery and execution.
16
+ * - ``composerBanner`` — directly above the composer in BOTH the full
17
+ * chat (``ChatContent``) and the floating bubble panel. It renders
18
+ * inside a fixed (non-scrolling) footer, so it never scrolls away
19
+ * with the thread.
20
+ * - ``contextDetails`` — grouped capabilities beneath the current context
21
+ * in the composer flyout. Call ``onDismiss`` before opening action dialogs.
22
+ * - ``launcherBadge`` — inside the collapsed chat-bubble launcher's
23
+ * wrapper (a positioned ``group`` container around the round button),
24
+ * so a badge can position itself absolutely over the button and use
25
+ * ``group-hover:`` styles for a hover flyout.
26
+ * - ``composerActions`` — in the composer's trailing control row, just
27
+ * before Send, in BOTH the full chat and the floating bubble panel.
28
+ * For controls that are an alternative *way to send* rather than
29
+ * content about the thread — a push-to-talk mic, say. Leading-edge
30
+ * controls (attach, context pins) stay owned by the composer itself.
31
+ *
32
+ * - ``threadSurface`` — rendered *instead of* the message viewport, for a
33
+ * mode that replaces the conversation rather than adding to it. A live
34
+ * voice call is the case it exists for: while you are talking, the
35
+ * transcript scrolling past is not what you are looking at. Unlike the
36
+ * other slots this one is set and cleared at runtime — registering a
37
+ * component IS the takeover, clearing it hands the thread back.
38
+ *
39
+ * A module that registers ``composerActions`` may also claim the Send
40
+ * position while the composer is empty (``composerActionsReplaceSend``).
41
+ * That is the shape a voice control wants: nothing has been typed, so Send
42
+ * has nothing to do, and the corner belongs to the other way of asking.
43
+ * Type one character and Send comes back. Opt-in, because a slot holding
44
+ * something unrelated must not make Send disappear.
45
+ */
46
+ import { useEffect, useState } from "react";
2
47
  import { create } from "zustand";
3
48
  import { useAuiState } from "@assistant-ui/react";
4
49
  export const useChatSlotsStore = create(() => ({
50
+ homeStarters: null,
5
51
  composerBanner: null,
6
52
  contextDetails: null,
7
53
  launcherBadge: null,
@@ -9,6 +55,23 @@ export const useChatSlotsStore = create(() => ({
9
55
  composerActionsReplaceSend: false,
10
56
  threadSurface: null,
11
57
  }));
58
+ const homeStarterHosts = new Map();
59
+ let homeStarterHostOrder = 0;
60
+ const useHomeStarterHostStore = create(() => ({ active: null }));
61
+ function refreshActiveHomeStarterHost() {
62
+ let active = null;
63
+ let bestPriority = Number.NEGATIVE_INFINITY;
64
+ let bestOrder = Number.NEGATIVE_INFINITY;
65
+ for (const [id, host] of homeStarterHosts) {
66
+ if (host.priority > bestPriority ||
67
+ (host.priority === bestPriority && host.order > bestOrder)) {
68
+ active = id;
69
+ bestPriority = host.priority;
70
+ bestOrder = host.order;
71
+ }
72
+ }
73
+ useHomeStarterHostStore.setState({ active });
74
+ }
12
75
  /**
13
76
  * Register chat slot components. Later calls overwrite only the keys
14
77
  * they pass, so independent modules can each claim a different slot.
@@ -17,6 +80,8 @@ export const useChatSlotsStore = create(() => ({
17
80
  */
18
81
  export function registerChatSlots(slots) {
19
82
  const next = {};
83
+ if (slots.homeStarters !== undefined)
84
+ next.homeStarters = slots.homeStarters;
20
85
  if (slots.composerBanner !== undefined)
21
86
  next.composerBanner = slots.composerBanner;
22
87
  if (slots.contextDetails !== undefined)
@@ -36,6 +101,20 @@ export function ChatComposerBannerSlot() {
36
101
  const Banner = useChatSlotsStore((s) => s.composerBanner);
37
102
  return Banner ? _jsx(Banner, {}) : null;
38
103
  }
104
+ export function ChatHomeStartersSlot({ priority = 0 }) {
105
+ const [hostId] = useState(() => Symbol("chat-home-starters"));
106
+ const active = useHomeStarterHostStore((state) => state.active);
107
+ const Starters = useChatSlotsStore((s) => s.homeStarters);
108
+ useEffect(() => {
109
+ homeStarterHosts.set(hostId, { priority, order: ++homeStarterHostOrder });
110
+ refreshActiveHomeStarterHost();
111
+ return () => {
112
+ homeStarterHosts.delete(hostId);
113
+ refreshActiveHomeStarterHost();
114
+ };
115
+ }, [hostId, priority]);
116
+ return active === hostId && Starters ? _jsx(Starters, {}) : null;
117
+ }
39
118
  export function ChatContextDetailsSlot({ onDismiss }) {
40
119
  const Details = useChatSlotsStore((s) => s.contextDetails);
41
120
  return Details ? _jsx(Details, { onDismiss: onDismiss }) : null;
@@ -1,9 +1,12 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChatEmptyState } from "./chat-empty-state.js";
3
+ import { useAppStore } from "../lib/app-store.js";
2
4
  import { Button, cn } from "@iloveagents/foundry-web-primitives";
3
5
  import { X, PanelLeft, MessageSquare } from "lucide-react";
4
6
  import { TooltipIconButton } from "./tooltip-icon-button.js";
5
7
  import { FocusPaneResize } from "./data-table/focus-pane-resize.js";
6
8
  import { ChatContent } from "./assistant-chat.js";
7
9
  export function FocusChatPane({ chatOpen, setChatOpen, chatWidth, setChatWidth, source, onClose, }) {
8
- return (_jsxs("aside", { "aria-label": "Focused content chat", style: { "--focus-chat-width": `${chatWidth ?? 384}px` }, className: cn("relative flex min-h-0 shrink-0 flex-col border-r border-border bg-background", chatOpen ? "w-full md:w-[var(--focus-chat-width)]" : "w-11", source && "hidden xl:flex"), children: [_jsx("div", { className: cn("flex h-12 shrink-0 items-center", chatOpen ? "gap-1 border-b border-border px-2" : "justify-center"), children: chatOpen ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "min-w-0 flex-1 px-2 text-sm font-semibold", children: "Chat" }), _jsx(TooltipIconButton, { tooltip: "Collapse chat", "aria-label": "Collapse chat", className: "size-8", onClick: () => setChatOpen(false), children: _jsx(PanelLeft, { size: 18 }) }), _jsx(Button, { variant: "ghost", size: "icon", title: "Close", "aria-label": "Close", className: "size-8 md:hidden", onClick: () => onClose(), children: _jsx(X, { size: 18 }) })] })) : (_jsx(TooltipIconButton, { tooltip: "Expand chat", "aria-label": "Expand chat", "aria-expanded": false, className: "size-8", onClick: () => setChatOpen(true), children: _jsx(MessageSquare, { size: 18 }) })) }), chatOpen && (_jsx(FocusPaneResize, { label: "chat", side: "right", width: chatWidth, onResize: setChatWidth })), chatOpen && (_jsx(ChatContent, { compactComposer: true, showPinButton: false, hideCurrentPageBadge: true, starterSuggestions: [] }))] }));
10
+ const pageLabel = useAppStore((state) => state.navContext.label);
11
+ return (_jsxs("aside", { "aria-label": "Focused content chat", style: { "--focus-chat-width": `${chatWidth ?? 384}px` }, className: cn("relative flex min-h-0 shrink-0 flex-col border-r border-border bg-background", chatOpen ? "w-full md:w-[var(--focus-chat-width)]" : "w-11", source && "hidden xl:flex"), children: [_jsx("div", { className: cn("flex h-12 shrink-0 items-center", chatOpen ? "gap-1 border-b border-border px-2" : "justify-center"), children: chatOpen ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "min-w-0 flex-1 px-2 text-sm font-semibold", children: "Chat" }), _jsx(TooltipIconButton, { tooltip: "Collapse chat", "aria-label": "Collapse chat", className: "size-8", onClick: () => setChatOpen(false), children: _jsx(PanelLeft, { size: 18 }) }), _jsx(Button, { variant: "ghost", size: "icon", title: "Close", "aria-label": "Close", className: "size-8 md:hidden", onClick: () => onClose(), children: _jsx(X, { size: 18 }) })] })) : (_jsx(TooltipIconButton, { tooltip: "Expand chat", "aria-label": "Expand chat", "aria-expanded": false, className: "size-8", onClick: () => setChatOpen(true), children: _jsx(MessageSquare, { size: 18 }) })) }), chatOpen && (_jsx(FocusPaneResize, { label: "chat", side: "right", width: chatWidth, onResize: setChatWidth })), chatOpen && (_jsx(ChatContent, { emptyState: _jsx(ChatEmptyState, { pageLabel: pageLabel, homeStarterPriority: 20 }), compactComposer: true, showPinButton: false, hideCurrentPageBadge: true, starterSuggestions: [] }))] }));
9
12
  }
@@ -3,5 +3,5 @@ import { Pin } from "lucide-react";
3
3
  import { cn } from "@iloveagents/foundry-web-primitives";
4
4
  /** State glyph; controls retain explicit Pin/Unpin action labels. */
5
5
  export function PinStateIcon({ pinned, className }) {
6
- return (_jsx(Pin, { "aria-hidden": "true", className: cn("size-4", pinned ? "text-primary fill-primary/25" : "text-muted-foreground", className) }));
6
+ return (_jsx(Pin, { "aria-hidden": "true", className: cn("size-4", pinned ? "text-primary fill-primary/20" : "rotate-45 text-muted-foreground fill-current/10", className) }));
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import type { ComponentProps } from "react";
2
2
  import { Button } from "@iloveagents/foundry-web-primitives";
3
- /** Pin state uses one upright glyph everywhere; the label describes the action. */
3
+ /** Pinned glyphs are upright; unpinned glyphs tilt. Labels describe the action. */
4
4
  export declare function PinToggle({ pinned, label, className, ...props }: Omit<ComponentProps<typeof Button>, "children"> & {
5
5
  pinned: boolean;
6
6
  label: string;
@@ -1,10 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { PinStateIcon } from "./pin-state-icon.js";
3
3
  import { Button, cn } from "@iloveagents/foundry-web-primitives";
4
- /** Pin state uses one upright glyph everywhere; the label describes the action. */
4
+ /** Pinned glyphs are upright; unpinned glyphs tilt. Labels describe the action. */
5
5
  export function PinToggle({ pinned, label, className, ...props }) {
6
6
  const action = `${pinned ? "Unpin" : "Pin"} ${label}`;
7
- return (_jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": action, "aria-pressed": pinned, title: action, ...props, className: cn("shrink-0 shadow-none", pinned
8
- ? "bg-primary/10 text-primary hover:bg-primary/15 hover:text-primary"
9
- : "text-muted-foreground", className), children: _jsx(PinStateIcon, { pinned: pinned }) }));
7
+ return (_jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": action, "aria-pressed": pinned, title: action, ...props, className: cn("shrink-0 shadow-none", pinned ? "text-primary hover:text-primary" : "text-muted-foreground", className), children: _jsx(PinStateIcon, { pinned: pinned }) }));
10
8
  }
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export { ChatContent, DEFAULT_STARTER_SUGGESTIONS, DEFAULT_COMPOSER_PLACEHOLDER,
10
10
  export { ComposerAddMenu, type ComposerAddMenuProps } from "./components/composer-add-menu.js";
11
11
  export type { ChatContentProps } from "./components/assistant-chat.js";
12
12
  export { ChatBubble } from "./components/chat-bubble.js";
13
- export { registerChatSlots, useChatSlotsStore, ChatComposerBannerSlot, ChatLauncherBadgeSlot, ChatComposerActionsSlot, useComposerActionsOwnSend, useChatThreadSurface, } from "./components/chat-slots.js";
13
+ export { registerChatSlots, useChatSlotsStore, ChatComposerBannerSlot, ChatHomeStartersSlot, ChatLauncherBadgeSlot, ChatComposerActionsSlot, useComposerActionsOwnSend, useChatThreadSurface, } from "./components/chat-slots.js";
14
14
  export { ChatHeader } from "./components/chat-header.js";
15
15
  export { ComposerAttachment, UserMessageAttachment, userAttachmentComponents, composerAttachmentComponents, } from "./components/chat-attachments.js";
16
16
  export { ShowDocumentToolUI } from "./components/show-document-tool-ui.js";
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ export { AGUIRuntimeProvider, useAGUIAdapter } from "./components/ag-ui-runtime-
9
9
  export { ChatContent, DEFAULT_STARTER_SUGGESTIONS, DEFAULT_COMPOSER_PLACEHOLDER, } from "./components/assistant-chat.js";
10
10
  export { ComposerAddMenu } from "./components/composer-add-menu.js";
11
11
  export { ChatBubble } from "./components/chat-bubble.js";
12
- export { registerChatSlots, useChatSlotsStore, ChatComposerBannerSlot, ChatLauncherBadgeSlot, ChatComposerActionsSlot, useComposerActionsOwnSend, useChatThreadSurface, } from "./components/chat-slots.js";
12
+ export { registerChatSlots, useChatSlotsStore, ChatComposerBannerSlot, ChatHomeStartersSlot, ChatLauncherBadgeSlot, ChatComposerActionsSlot, useComposerActionsOwnSend, useChatThreadSurface, } from "./components/chat-slots.js";
13
13
  export { ChatHeader } from "./components/chat-header.js";
14
14
  export { ComposerAttachment, UserMessageAttachment, userAttachmentComponents, composerAttachmentComponents, } from "./components/chat-attachments.js";
15
15
  export { ShowDocumentToolUI } from "./components/show-document-tool-ui.js";
@@ -13,13 +13,16 @@ export function summarizeChatContext(state) {
13
13
  return { ...item, ...payload, meta: { ...record(item.meta), ...record(payload.meta) } };
14
14
  });
15
15
  const field = references.find((item) => item.meta.fieldFocus === "active");
16
+ const component = references.find((item) => item.meta.componentFocus === "active");
16
17
  const selected = references.find((item) => item.type === "selection" || item.meta.selectionScope === "text");
17
18
  const target = field && record(field.target ?? field.meta.jsonTarget);
18
19
  const detail = selected
19
20
  ? "Selected text"
20
21
  : field
21
22
  ? text(target?.label) || text(field.meta.fieldLabel) || text(field.label)
22
- : "";
23
+ : component
24
+ ? text(component.meta.componentLabel) || text(component.label)
25
+ : "";
23
26
  const pageLabel = nav.label && !nav.label.startsWith("/") && !/^\/?chat$/i.test(nav.label)
24
27
  ? nav.label
25
28
  : "Your spaces";
@@ -27,7 +30,7 @@ export function summarizeChatContext(state) {
27
30
  const open = references.filter((item) => item.meta.windowState === "open");
28
31
  return {
29
32
  label,
30
- detail,
33
+ detail: detail === label ? "" : detail,
31
34
  focused: Boolean(focused.title),
32
35
  path: nav.page,
33
36
  field: Boolean(field),
@@ -1,6 +1,7 @@
1
1
  import { useChatWidth } from "./use-chat-width.js";
2
- export declare function WorkbenchChatHeader({ collapsed, chatWidth, buttonClass, toggleNavigation, setCollapsed, showHistory, startChat, returnToChat, openHistory, chatOnly, setContentCollapsed, }: {
2
+ export declare function WorkbenchChatHeader({ collapsed, running, chatWidth, buttonClass, toggleNavigation, setCollapsed, showHistory, startChat, returnToChat, openHistory, chatOnly, setContentCollapsed, }: {
3
3
  collapsed: boolean;
4
+ running: boolean;
4
5
  chatWidth: ReturnType<typeof useChatWidth>;
5
6
  buttonClass: string;
6
7
  toggleNavigation: () => void;
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { TooltipIconButton } from "../index.js";
3
- import { SquarePen, PanelLeft, MessageSquare, Menu, History } from "lucide-react";
4
- export function WorkbenchChatHeader({ collapsed, chatWidth, buttonClass, toggleNavigation, setCollapsed, showHistory, startChat, returnToChat, openHistory, chatOnly, setContentCollapsed, }) {
5
- return (_jsxs(_Fragment, { children: [_jsxs("div", { "data-chat-header": true, className: `relative flex shrink-0 items-center ${collapsed ? "justify-center" : "gap-1 px-2"}`, children: [!collapsed && !chatWidth.compact && (_jsx(TooltipIconButton, { tooltip: "Open navigation", className: `${buttonClass} preview-mobile-navigation`, "aria-label": "Open navigation", onClick: toggleNavigation, children: _jsx(Menu, { size: 18 }) })), collapsed ? (_jsx(TooltipIconButton, { tooltip: "Expand chat", className: buttonClass, "aria-label": "Expand chat", onClick: () => setCollapsed(false), children: _jsx(MessageSquare, { size: 18 }) })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "min-w-0 flex-1 px-2 py-2", children: _jsx("div", { className: "text-sm font-semibold", children: showHistory ? "Conversations" : "Chat" }) }), _jsx(TooltipIconButton, { tooltip: "New chat", className: buttonClass, "aria-label": "New chat", onClick: startChat, children: _jsx(SquarePen, { size: 18 }) }), _jsx(TooltipIconButton, { tooltip: "Chat history", className: buttonClass, "aria-label": "Chat history", "aria-expanded": showHistory, onClick: () => (showHistory ? returnToChat() : openHistory()), children: _jsx(History, { size: 18 }) }), _jsx(TooltipIconButton, { tooltip: "Collapse chat", style: chatOnly || chatWidth.compact ? { display: "none" } : undefined, className: buttonClass, "aria-label": "Collapse chat", onClick: () => {
3
+ import { SquarePen, PanelLeft, MessageSquare, Menu, History, LoaderCircle } from "lucide-react";
4
+ export function WorkbenchChatHeader({ collapsed, running, chatWidth, buttonClass, toggleNavigation, setCollapsed, showHistory, startChat, returnToChat, openHistory, chatOnly, setContentCollapsed, }) {
5
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { "data-chat-header": true, className: `relative flex shrink-0 items-center ${collapsed ? "justify-center" : "gap-1 px-2"}`, children: [!collapsed && !chatWidth.compact && (_jsx(TooltipIconButton, { tooltip: "Open navigation", className: `${buttonClass} preview-mobile-navigation`, "aria-label": "Open navigation", onClick: toggleNavigation, children: _jsx(Menu, { size: 18 }) })), collapsed ? (_jsx(TooltipIconButton, { tooltip: running ? "AI is working — expand chat" : "Expand chat", className: buttonClass, "aria-label": running ? "AI is working — expand chat" : "Expand chat", onClick: () => setCollapsed(false), children: running ? (_jsx(LoaderCircle, { size: 18, className: "animate-spin motion-reduce:animate-none text-primary" })) : (_jsx(MessageSquare, { size: 18 })) })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "min-w-0 flex-1 px-2 py-2", children: _jsx("div", { className: "text-sm font-semibold", children: showHistory ? "Conversations" : "Chat" }) }), _jsx(TooltipIconButton, { tooltip: "New chat", className: buttonClass, "aria-label": "New chat", onClick: startChat, children: _jsx(SquarePen, { size: 18 }) }), _jsx(TooltipIconButton, { tooltip: "Chat history", className: buttonClass, "aria-label": "Chat history", "aria-expanded": showHistory, onClick: () => (showHistory ? returnToChat() : openHistory()), children: _jsx(History, { size: 18 }) }), _jsx(TooltipIconButton, { tooltip: "Collapse chat", style: chatOnly || chatWidth.compact ? { display: "none" } : undefined, className: buttonClass, "aria-label": "Collapse chat", onClick: () => {
6
6
  setContentCollapsed(false);
7
7
  setCollapsed(true);
8
8
  }, children: _jsx(PanelLeft, { size: 18 }) })] }))] }), collapsed && (_jsxs("div", { "data-chat-rail": true, className: "flex flex-col items-center gap-1", children: [_jsx(TooltipIconButton, { tooltip: "Open navigation", className: `${buttonClass} preview-mobile-navigation`, "aria-label": "Open navigation", onClick: toggleNavigation, children: _jsx(Menu, { size: 18 }) }), _jsx(TooltipIconButton, { tooltip: "Chat history", className: buttonClass, "aria-label": "Chat history", onClick: () => {
@@ -1,6 +1,6 @@
1
1
  import type { NavGroup } from "../index.js";
2
2
  /** History is a view of existing navigation targets, never a second chat runtime. */
3
- export declare function ConversationHistory({ groups, workspaceId, onClose, showBack, welcome, compact, onViewAll, }: {
3
+ export declare function ConversationHistory({ groups, workspaceId, onClose, showBack, welcome, compact, onViewAll, entityId, rootContext, }: {
4
4
  groups: NavGroup[];
5
5
  workspaceId: unknown;
6
6
  onClose: () => void;
@@ -8,4 +8,7 @@ export declare function ConversationHistory({ groups, workspaceId, onClose, show
8
8
  welcome?: boolean;
9
9
  compact?: boolean;
10
10
  onViewAll?: () => void;
11
+ /** Current content entity. Compact homes show only an exact match unless this is the workspace root. */
12
+ entityId?: unknown;
13
+ rootContext?: boolean;
11
14
  }): import("react/jsx-runtime").JSX.Element;
@@ -3,9 +3,9 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuIte
3
3
  import { Button, Input } from "@iloveagents/foundry-web-primitives";
4
4
  import { Fragment, useEffect, useState } from "react";
5
5
  import { Link } from "react-router";
6
- import { ArrowLeft, MessageSquare, MoreHorizontal, Pin, Loader2, ChevronDown, Check, } from "lucide-react";
6
+ import { ArrowLeft, MessageSquare, MoreHorizontal, Pin, Loader2, ChevronDown, Check, History, } from "lucide-react";
7
7
  /** History is a view of existing navigation targets, never a second chat runtime. */
8
- export function ConversationHistory({ groups, workspaceId, onClose, showBack = true, welcome = false, compact = false, onViewAll, }) {
8
+ export function ConversationHistory({ groups, workspaceId, onClose, showBack = true, welcome = false, compact = false, onViewAll, entityId, rootContext = false, }) {
9
9
  const [loadingTo, setLoadingTo] = useState(null);
10
10
  const [loadError, setLoadError] = useState(null);
11
11
  const [all, setAll] = useState(!workspaceId);
@@ -16,9 +16,12 @@ export function ConversationHistory({ groups, workspaceId, onClose, showBack = t
16
16
  groups
17
17
  .filter((g) => g.meta?.type === "recent-chats" || g.meta?.type === "pinned-chats")
18
18
  .flatMap((g) => g.items);
19
- const scopedItems = all || !workspaceId
19
+ const workspaceItems = all || !workspaceId
20
20
  ? allItems
21
21
  : (scoped?.meta?.workspaceConversations ?? scoped?.items ?? []);
22
+ const scopedItems = compact && workspaceId && !rootContext
23
+ ? workspaceItems.filter((item) => Boolean(entityId) && item.meta?.previewEntityId === entityId)
24
+ : workspaceItems;
22
25
  const items = [...new Map(scopedItems.map((item) => [item.to, item])).values()]
23
26
  .filter((item) => item.meta?.type !== "show-all-chats")
24
27
  .filter((item) => item.label.toLocaleLowerCase().includes(search.toLocaleLowerCase()))
@@ -31,7 +34,7 @@ export function ConversationHistory({ groups, workspaceId, onClose, showBack = t
31
34
  { label: "Recent chats", items: visibleItems.filter((item) => !item.meta?.pinned) },
32
35
  ]
33
36
  : [{ label: "", items: visibleItems }];
34
- return (_jsxs("section", { "data-conversation-history": true, "aria-label": "Chat history", className: "mx-auto flex min-h-0 w-full max-w-3xl flex-1 flex-col px-4 pb-4", children: [welcome && (_jsxs("div", { className: "pb-6 pt-8", children: [_jsx("h1", { className: "text-xl font-semibold", children: "How can I help?" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Start something new or continue a recent chat." })] })), showBack && (_jsxs("button", { onClick: onClose, className: "mb-4 flex items-center gap-2 py-2 text-xs text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { size: 14 }), " Back to chat"] })), _jsxs("div", { className: "mb-3 mt-2 flex items-center justify-between", children: [_jsx("h2", { className: compact ? "text-xs font-medium text-muted-foreground" : "text-base font-semibold", children: "Conversation history" }), compact && (_jsx("button", { onClick: onViewAll, className: "text-xs text-muted-foreground hover:text-foreground", children: "View all" }))] }), Boolean(workspaceId) && (_jsx("div", { className: "mb-3", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", "aria-label": "History filter", className: "w-full justify-between font-normal shadow-none", children: [_jsx("span", { className: "truncate", children: all ? "All conversations" : "Last used in this workspace" }), _jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "w-[var(--radix-dropdown-menu-trigger-width)]", children: [
37
+ return (_jsxs("section", { "data-conversation-history": true, "aria-label": compact ? "Recent conversations" : "Chat history", className: compact ? "w-full" : "mx-auto flex min-h-0 w-full max-w-3xl flex-1 flex-col px-4 pb-4", children: [welcome && (_jsxs("div", { className: "pb-6 pt-8", children: [_jsx("h1", { className: "text-xl font-semibold", children: "How can I help?" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Start something new or continue a recent chat." })] })), showBack && (_jsxs("button", { onClick: onClose, className: "mb-4 flex items-center gap-2 py-2 text-xs text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { size: 14 }), " Back to chat"] })), (!compact || items.length > 0) && (_jsx("div", { className: "mb-2 mt-2 flex items-center justify-between", children: _jsx("h2", { className: compact ? "text-xs font-medium text-muted-foreground" : "text-base font-semibold", children: compact ? "Recent conversations" : "Conversation history" }) })), Boolean(workspaceId) && !compact && (_jsx("div", { className: "mb-3", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", "aria-label": "History filter", className: "w-full justify-between font-normal shadow-none", children: [_jsx("span", { className: "truncate", children: all ? "All conversations" : "Last used in this workspace" }), _jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "w-[var(--radix-dropdown-menu-trigger-width)]", children: [
35
38
  { value: false, label: "Last used in this workspace" },
36
39
  { value: true, label: "All conversations" },
37
40
  ].map((option) => (_jsxs(DropdownMenuItem, { role: "menuitemradio", "aria-checked": all === option.value, onSelect: () => setAll(option.value), children: [_jsx("span", { className: "flex-1", children: option.label }), all === option.value && _jsx(Check, { className: "size-4", "aria-hidden": "true" })] }, option.label))) })] }) })), !compact && (_jsx(Input, { "aria-label": "Search chats", placeholder: "Search chats", value: search, onChange: (event) => setSearch(event.target.value), className: "mb-3" })), loadError && (_jsx("p", { role: "alert", className: "mb-2 text-xs text-destructive", children: loadError })), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", "aria-busy": Boolean(loadingTo), children: [sections
@@ -79,5 +82,5 @@ export function ConversationHistory({ groups, workspaceId, onClose, showBack = t
79
82
  ? "Location unavailable"
80
83
  : "Last outside a workspace" })] })] }), Boolean(item.actions?.length) && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": `Actions for ${item.label}`, className: "mr-1 inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 data-[state=open]:opacity-100 [@media(hover:none)]:opacity-100 hover:bg-background", children: _jsx(MoreHorizontal, { className: "size-4" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: item.actions?.map((action) => (_jsxs(Fragment, { children: [action.separator && _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: () => {
81
84
  void action.handler();
82
- }, className: action.destructive ? "text-destructive" : undefined, children: [_jsx(action.icon, { className: "mr-2 size-4" }), action.label] })] }, action.label))) })] }))] }, item.to)))] }, section.label))), items.length === 0 && (_jsx("p", { className: "py-3 text-sm text-muted-foreground", children: search ? "No matching chats" : "No chats yet" }))] })] }));
85
+ }, className: action.destructive ? "text-destructive" : undefined, children: [_jsx(action.icon, { className: "mr-2 size-4" }), action.label] })] }, action.label))) })] }))] }, item.to)))] }, section.label))), items.length === 0 && !compact && (_jsx("p", { className: "py-3 text-sm text-muted-foreground", children: search ? "No matching chats" : "No chats yet" }))] }), compact && onViewAll && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: onViewAll, className: "mt-1 h-9 w-fit gap-2 px-2 text-sm font-normal text-muted-foreground [@media(pointer:coarse)]:min-h-11", children: [_jsx(History, { className: "size-4", "aria-hidden": true }), "Conversation history"] }))] }));
83
86
  }
@@ -1,7 +1,11 @@
1
+ import type { NavItem } from "../lib/nav-config.js";
2
+ export declare function normalizeWorkbenchPath(path: string): string;
1
3
  export declare function useWorkbenchState(chatRoute: boolean): {
2
4
  panelOpen: boolean;
5
+ currentPage: string;
3
6
  chatOnly: boolean;
4
7
  pageLabel: string;
8
+ entityId: unknown;
5
9
  chatWidth: {
6
10
  min: number;
7
11
  max: number;
@@ -12,6 +16,7 @@ export declare function useWorkbenchState(chatRoute: boolean): {
12
16
  compact: boolean;
13
17
  };
14
18
  collapsed: boolean;
19
+ running: boolean;
15
20
  setCollapsed: import("react").Dispatch<import("react").SetStateAction<boolean>>;
16
21
  compactPane: "content" | "chat";
17
22
  setCompactPane: import("react").Dispatch<import("react").SetStateAction<"content" | "chat">>;
@@ -33,7 +38,7 @@ export declare function useWorkbenchState(chatRoute: boolean): {
33
38
  label: string;
34
39
  tree?: boolean;
35
40
  icon?: import("lucide-react").LucideIcon;
36
- items: import("../index.js").NavItem[];
41
+ items: NavItem[];
37
42
  priority?: number;
38
43
  defaultCollapsed?: boolean;
39
44
  to?: string;
@@ -6,6 +6,20 @@ import { Button } from "@iloveagents/foundry-web-primitives";
6
6
  import { useSidebarStore, useChatBubbleStore, useToolPanelStore, useNewConversation, useNavStore, useAppStore, } from "../index.js";
7
7
  import { History, Settings, Layers } from "lucide-react";
8
8
  import { useChatWidth } from "./use-chat-width.js";
9
+ export function normalizeWorkbenchPath(path) {
10
+ return path === "/" ? path : path.replace(/\/+$/, "");
11
+ }
12
+ function findExactWorkbenchItem(items, pathname) {
13
+ const target = normalizeWorkbenchPath(pathname);
14
+ for (const item of items) {
15
+ if (normalizeWorkbenchPath(item.to) === target)
16
+ return item;
17
+ const nested = item.children && findExactWorkbenchItem(item.children, pathname);
18
+ if (nested)
19
+ return nested;
20
+ }
21
+ return undefined;
22
+ }
9
23
  function useWorkbenchNavigation() {
10
24
  const navGroups = useNavStore((s) => s.config);
11
25
  const workspaceGroups = useMemo(() => {
@@ -57,7 +71,12 @@ export function useWorkbenchState(chatRoute) {
57
71
  const panelOpen = useToolPanelStore((s) => s.isOpen && !s.isWindowHosted);
58
72
  const chatOnly = chatRoute && !panelOpen;
59
73
  const location = useLocation();
74
+ const { navGroups, workspaceGroups, staticNavigationGroups } = useWorkbenchNavigation();
75
+ const routeItem = useMemo(() => navGroups
76
+ .map((group) => findExactWorkbenchItem(group.items, location.pathname))
77
+ .find((item) => item !== undefined), [location.pathname, navGroups]);
60
78
  const pageLabel = useAppStore((s) => s.navContext.label);
79
+ const entityId = routeItem?.meta?.spacesEntityId;
61
80
  const chatWidth = useChatWidth();
62
81
  const [desktopCollapsed, setCollapsed] = useState(false);
63
82
  const collapsed = !chatWidth.compact && desktopCollapsed;
@@ -73,6 +92,7 @@ export function useWorkbenchState(chatRoute) {
73
92
  // the same address again. Collapsing remains local until the next entry.
74
93
  }, [chatOnly, location.key]);
75
94
  const [historyOpen, setHistoryOpen] = useState(false);
95
+ const running = useAuiState((s) => s.thread.isRunning);
76
96
  const emptyChat = useAuiState((s) => s.thread.messages.length === 0 && !s.thread.isRunning && !s.thread.isLoading);
77
97
  const wasEmptyChat = useRef(emptyChat);
78
98
  useEffect(() => {
@@ -88,9 +108,8 @@ export function useWorkbenchState(chatRoute) {
88
108
  const openHistory = () => {
89
109
  setHistoryOpen(true);
90
110
  };
91
- const historyShortcut = (_jsxs(Button, { variant: "ghost", size: "sm", onClick: openHistory, className: "h-8 w-fit gap-2 px-3 text-xs font-normal text-muted-foreground [@media(pointer:coarse)]:min-h-11", children: [_jsx(History, { className: "size-3.5", "aria-hidden": true }), "Conversation history"] }));
111
+ const historyShortcut = (_jsxs(Button, { variant: "ghost", size: "sm", onClick: openHistory, className: "h-9 w-fit gap-2 px-2 text-sm font-normal text-muted-foreground [@media(pointer:coarse)]:min-h-11", children: [_jsx(History, { className: "size-4", "aria-hidden": true }), "Conversation history"] }));
92
112
  const toggleNavigation = useSidebarStore((s) => s.toggleMobile);
93
- const { navGroups, workspaceGroups, staticNavigationGroups } = useWorkbenchNavigation();
94
113
  useEffect(() => {
95
114
  if (panelOpen) {
96
115
  setContentCollapsed(false);
@@ -118,10 +137,13 @@ export function useWorkbenchState(chatRoute) {
118
137
  }, [requestedChat]);
119
138
  return {
120
139
  panelOpen,
140
+ currentPage: location.pathname,
121
141
  chatOnly,
122
142
  pageLabel,
143
+ entityId,
123
144
  chatWidth,
124
145
  collapsed,
146
+ running,
125
147
  setCollapsed,
126
148
  compactPane,
127
149
  setCompactPane,
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button } from "@iloveagents/foundry-web-primitives";
3
3
  import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, } from "../index.js";
4
+ import { ChatHomeStartersSlot } from "../components/chat-slots.js";
4
5
  import { Layers } from "lucide-react";
5
6
  export function WelcomeIntro({ navGroups, historyShortcut, }) {
6
- return (_jsxs("div", { className: "mb-4 text-center", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: "What would you like to get done?" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Find information, create or edit content, and run actions across your spaces." }), _jsxs("div", { className: "mt-4 flex flex-wrap items-center justify-center gap-2", children: [historyShortcut, navGroups.some((group) => group.meta?.type === "spaces-workspaces" && group.createActions?.length) && (_jsx("div", { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 gap-2 px-3 text-xs font-normal text-muted-foreground [@media(pointer:coarse)]:min-h-11", children: [_jsx(Layers, { className: "size-3.5", "aria-hidden": "true" }), "Create a space"] }) }), _jsx(DropdownMenuContent, { align: "center", children: navGroups
7
+ return (_jsxs("div", { className: "mb-4 text-center", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: "What would you like to get done?" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Find information, create or edit content, and run actions across your spaces." }), _jsx("div", { className: "mx-auto mt-4 max-w-sm text-left", children: _jsx(ChatHomeStartersSlot, { priority: 10 }) }), _jsxs("div", { className: "mt-4 flex flex-wrap items-center justify-center gap-2", children: [historyShortcut, navGroups.some((group) => group.meta?.type === "spaces-workspaces" && group.createActions?.length) && (_jsx("div", { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 gap-2 px-3 text-xs font-normal text-muted-foreground [@media(pointer:coarse)]:min-h-11", children: [_jsx(Layers, { className: "size-3.5", "aria-hidden": "true" }), "Create a space"] }) }), _jsx(DropdownMenuContent, { align: "center", children: navGroups
7
8
  .filter((group) => group.meta?.type === "spaces-workspaces")
8
9
  .flatMap((group) => group.createActions ?? [])
9
10
  .map((action) => (_jsx(DropdownMenuItem, { onSelect: () => action.handler(), children: action.label }, action.id))) })] }) }))] })] }));
@@ -1,21 +1,32 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { WorkbenchNavigation } from "./navigation.js";
3
3
  import { WorkbenchChatHeader } from "./chat-header.js";
4
4
  import { WorkbenchContentPane } from "./content-pane.js";
5
5
  import { ChatResizeHandle } from "./chat-resize-handle.js";
6
+ import { ChatEmptyState } from "../components/chat-empty-state.js";
6
7
  import { WelcomeIntro } from "./welcome-intro.js";
7
- import { useWorkbenchState } from "./use-workbench-state.js";
8
+ import { normalizeWorkbenchPath, useWorkbenchState } from "./use-workbench-state.js";
8
9
  import { WorkbenchStyles } from "./workbench-styles.js";
9
10
  import { Button } from "@iloveagents/foundry-web-primitives";
10
11
  import { ChatContent, TooltipIconButton } from "../index.js";
11
- import { FileText, MessageSquare, Menu } from "lucide-react";
12
+ import { FileText, MessageSquare, Menu, LoaderCircle } from "lucide-react";
12
13
  import { ConversationHistory } from "./conversation-history.js";
13
14
  export function WorkbenchPreview({ page, chatOnly: chatRoute = false, }) {
14
- const { panelOpen, chatOnly, pageLabel, chatWidth, collapsed, setCollapsed, compactPane, setCompactPane, contentCollapsed, setContentCollapsed, contentHidden, historyOpen, emptyChat, showHistory, spaciousChat, returnToChat, openHistory, historyShortcut, toggleNavigation, navGroups, workspaceGroups, staticNavigationGroups, closePanel, startChat, } = useWorkbenchState(chatRoute);
15
+ const { panelOpen, currentPage, chatOnly, pageLabel, entityId, chatWidth, collapsed, running, setCollapsed, compactPane, setCompactPane, contentCollapsed, setContentCollapsed, contentHidden, historyOpen, emptyChat, showHistory, spaciousChat, returnToChat, openHistory, historyShortcut, toggleNavigation, navGroups, workspaceGroups, staticNavigationGroups, closePanel, startChat, } = useWorkbenchState(chatRoute);
16
+ const workspaceId = workspaceGroups?.[0]?.meta?.exclusiveRootId;
17
+ const rootItem = workspaceGroups?.[0]
18
+ ? (workspaceGroups[0].items.find((item) => item.to === workspaceGroups[0].focusOn) ??
19
+ workspaceGroups[0].items[0])
20
+ : undefined;
21
+ const rootContext = Boolean(workspaceId &&
22
+ rootItem &&
23
+ normalizeWorkbenchPath(rootItem.to) === normalizeWorkbenchPath(currentPage));
24
+ const conversationHome = (_jsx(ConversationHistory, { groups: navGroups, workspaceId: workspaceId, entityId: entityId, rootContext: rootContext, onClose: returnToChat, onViewAll: openHistory, showBack: false, compact: true }));
25
+ const emptyChatFooter = workspaceId ? conversationHome : historyShortcut;
15
26
  const buttonClass = "inline-flex items-center justify-center rounded-lg p-2 text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-primary";
16
27
  return (_jsxs("div", { className: "workbench-preview flex min-h-0 flex-1 overflow-hidden", "data-workspace-navigation": workspaceGroups ? "true" : undefined, children: [_jsx(WorkbenchStyles, {}), _jsx(WorkbenchNavigation, { groups: staticNavigationGroups, workspace: !!workspaceGroups, onNewConversation: startChat }), _jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden", "data-workbench-surface": true, role: "region", "aria-label": "Workspace working area", children: [chatWidth.compact && (_jsxs("div", { className: "flex shrink-0 items-center gap-1 border-b border-border px-2 py-1", role: "group", "aria-label": "Workspace panes", children: [_jsx(Button, { variant: "ghost", size: "icon", className: "preview-mobile-navigation size-11", "aria-label": "Open navigation", onClick: toggleNavigation, children: _jsx(Menu, { className: "size-4" }) }), ["content", "chat"]
17
28
  .filter((pane) => !chatOnly || pane === "chat")
18
- .map((pane) => (_jsxs(Button, { variant: "ghost", className: "h-11 gap-1.5 px-3 aria-pressed:bg-muted", "aria-pressed": chatOnly || compactPane === pane, "aria-controls": pane === "chat" ? "preview-assistant" : "preview-content", onClick: () => setCompactPane(pane), children: [pane === "chat" ? (_jsx(MessageSquare, { className: "size-4" })) : (_jsx(FileText, { className: "size-4" })), pane === "chat" ? "Chat" : "Content"] }, pane)))] })), _jsxs("div", { ref: chatWidth.containerRef, className: "relative flex min-h-0 min-w-0 flex-1 overflow-x-auto", children: [_jsxs("section", { id: "preview-assistant", "data-panel-motion": true, "data-global-chat": spaciousChat || undefined, "data-chat-collapsed": collapsed ? "true" : undefined, inert: chatWidth.compact && !chatOnly && compactPane !== "chat", "aria-label": "Assistant conversation", className: "relative flex min-h-0 shrink-0 flex-col", style: {
29
+ .map((pane) => (_jsxs(Button, { variant: "ghost", className: "h-11 gap-1.5 px-3 aria-pressed:bg-muted", "aria-pressed": chatOnly || compactPane === pane, "aria-controls": pane === "chat" ? "preview-assistant" : "preview-content", "aria-label": pane === "chat" && running ? "Chat — AI is working" : undefined, onClick: () => setCompactPane(pane), children: [pane === "chat" ? (running ? (_jsx(LoaderCircle, { className: "size-4 animate-spin motion-reduce:animate-none text-primary" })) : (_jsx(MessageSquare, { className: "size-4" }))) : (_jsx(FileText, { className: "size-4" })), pane === "chat" ? "Chat" : "Content"] }, pane)))] })), _jsxs("div", { ref: chatWidth.containerRef, className: "relative flex min-h-0 min-w-0 flex-1 overflow-x-auto", children: [_jsxs("section", { id: "preview-assistant", "data-panel-motion": true, "data-global-chat": spaciousChat || undefined, "data-chat-collapsed": collapsed ? "true" : undefined, inert: chatWidth.compact && !chatOnly && compactPane !== "chat", "aria-label": "Assistant conversation", className: "relative flex min-h-0 shrink-0 flex-col", style: {
19
30
  display: chatWidth.compact && !chatOnly && compactPane !== "chat" ? "none" : "flex",
20
31
  width: chatWidth.compact || chatOnly
21
32
  ? "100%"
@@ -25,8 +36,8 @@ export function WorkbenchPreview({ page, chatOnly: chatRoute = false, }) {
25
36
  ? "calc(100% - 44px)"
26
37
  : chatWidth.width,
27
38
  background: "var(--background)",
28
- }, children: [_jsx(WorkbenchChatHeader, { collapsed: collapsed, chatWidth: chatWidth, buttonClass: buttonClass, toggleNavigation: toggleNavigation, setCollapsed: setCollapsed, showHistory: showHistory, startChat: startChat, returnToChat: returnToChat, openHistory: openHistory, chatOnly: chatOnly, setContentCollapsed: setContentCollapsed }), !collapsed && showHistory && (_jsx(ConversationHistory, { groups: navGroups, workspaceId: workspaceGroups?.[0]?.meta?.exclusiveRootId, onClose: returnToChat, showBack: true })), !chatWidth.compact && !collapsed && !contentCollapsed && (_jsx(ChatResizeHandle, { chatWidth: chatWidth })), _jsx("div", { className: "min-h-0 flex-1 flex-col", style: {
39
+ }, children: [_jsx(WorkbenchChatHeader, { collapsed: collapsed, running: running, chatWidth: chatWidth, buttonClass: buttonClass, toggleNavigation: toggleNavigation, setCollapsed: setCollapsed, showHistory: showHistory, startChat: startChat, returnToChat: returnToChat, openHistory: openHistory, chatOnly: chatOnly, setContentCollapsed: setContentCollapsed }), !collapsed && showHistory && (_jsx(ConversationHistory, { groups: navGroups, workspaceId: workspaceId, onClose: returnToChat, showBack: true })), !chatWidth.compact && !collapsed && !contentCollapsed && (_jsx(ChatResizeHandle, { chatWidth: chatWidth })), _jsx("div", { className: "min-h-0 flex-1 flex-col", style: {
29
40
  display: collapsed || showHistory ? "none" : "flex",
30
41
  paddingBottom: spaciousChat && emptyChat && !historyOpen ? "clamp(32px, 10vh, 96px)" : undefined,
31
- }, children: _jsx(ChatContent, { centerComposer: spaciousChat && emptyChat && !historyOpen, composerIntro: spaciousChat && emptyChat ? (_jsx(WelcomeIntro, { navGroups: navGroups, historyShortcut: historyShortcut })) : undefined, compactComposer: !spaciousChat, hideCurrentPageBadge: true, showPinButton: false, starterSuggestions: [], emptyState: _jsxs("div", { className: "flex min-h-full flex-col justify-center gap-3 px-6 py-8", children: [_jsx("h1", { className: "text-lg font-semibold tracking-tight", children: "What would you like to get done?" }), _jsx("p", { className: "max-w-sm text-sm leading-relaxed text-muted-foreground", children: pageLabel && !pageLabel.startsWith("/") ? (_jsxs(_Fragment, { children: ["Work with ", _jsx("span", { className: "font-medium text-foreground", children: pageLabel }), " ", "or across your spaces: find information, create or edit content, and run actions."] })) : ("Find information, create or edit content, and run actions across your spaces.") }), _jsx("p", { className: "max-w-sm text-xs leading-relaxed text-muted-foreground", children: "Your conversation continues as you browse. Select text or a field to work more specifically." }), historyShortcut] }) }) })] }), _jsx(WorkbenchContentPane, { contentHidden: contentHidden, panelOpen: panelOpen, chatWidth: chatWidth, contentCollapsed: contentCollapsed, collapsed: collapsed, buttonClass: buttonClass, setCollapsed: setCollapsed, setContentCollapsed: setContentCollapsed, closePanel: closePanel, page: page }), contentCollapsed && !chatOnly && !chatWidth.compact && (_jsx("aside", { "aria-label": "Collapsed content", className: "flex w-11 shrink-0 flex-col items-center", style: { width: 44 }, children: _jsx("div", { className: "flex h-full w-full flex-col items-center", "data-chat-rail": true, children: _jsx("div", { className: "flex items-center justify-center", style: { height: 60 }, children: _jsx(TooltipIconButton, { tooltip: "Expand content", className: buttonClass, "aria-label": "Expand content", onClick: () => setContentCollapsed(false), children: _jsx(FileText, { size: 18 }) }) }) }) }))] })] })] }));
42
+ }, children: _jsx(ChatContent, { centerComposer: spaciousChat && emptyChat && !historyOpen, composerIntro: spaciousChat && emptyChat ? (_jsx(WelcomeIntro, { navGroups: navGroups, historyShortcut: emptyChatFooter })) : undefined, compactComposer: !spaciousChat, hideCurrentPageBadge: true, showPinButton: false, starterSuggestions: [], emptyState: _jsx(ChatEmptyState, { pageLabel: pageLabel, showHomeStarters: !spaciousChat, children: emptyChatFooter }) }) })] }), _jsx(WorkbenchContentPane, { contentHidden: contentHidden, panelOpen: panelOpen, chatWidth: chatWidth, contentCollapsed: contentCollapsed, collapsed: collapsed, buttonClass: buttonClass, setCollapsed: setCollapsed, setContentCollapsed: setContentCollapsed, closePanel: closePanel, page: page }), contentCollapsed && !chatOnly && !chatWidth.compact && (_jsx("aside", { "aria-label": "Collapsed content", className: "flex w-11 shrink-0 flex-col items-center", style: { width: 44 }, children: _jsx("div", { className: "flex h-full w-full flex-col items-center", "data-chat-rail": true, children: _jsx("div", { className: "flex items-center justify-center", style: { height: 60 }, children: _jsx(TooltipIconButton, { tooltip: "Expand content", className: buttonClass, "aria-label": "Expand content", onClick: () => setContentCollapsed(false), children: _jsx(FileText, { size: 18 }) }) }) }) }))] })] })] }));
32
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-web-ui",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "license": "MIT",
5
5
  "description": "React agent UI core for Foundry UI — chat, composer, AG-UI adapter for assistant-ui, tool cards, panels, sidebar, theme runtime, and UI stores.",
6
6
  "keywords": [
@@ -80,8 +80,8 @@
80
80
  "recharts": "^3.10.1",
81
81
  "remark-gfm": "^4.0.0",
82
82
  "tailwind-merge": "^3.5.0",
83
- "@iloveagents/foundry-agent": "^0.28.0",
84
- "@iloveagents/foundry-web-primitives": "^0.28.0"
83
+ "@iloveagents/foundry-agent": "^0.29.0",
84
+ "@iloveagents/foundry-web-primitives": "^0.29.0"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@ag-ui/client": "^0.0.52",