@iloveagents/foundry-web-ui 0.26.0 → 0.27.1

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.
Files changed (82) hide show
  1. package/dist/components/assistant-chat.d.ts +13 -2
  2. package/dist/components/assistant-chat.js +19 -14
  3. package/dist/components/chat-bubble.js +9 -6
  4. package/dist/components/chat-context.d.ts +2 -0
  5. package/dist/components/chat-context.js +70 -0
  6. package/dist/components/chat-slots.d.ts +10 -0
  7. package/dist/components/chat-slots.js +7 -0
  8. package/dist/components/composer-add-menu.js +3 -2
  9. package/dist/components/composer-input.d.ts +4 -0
  10. package/dist/components/composer-input.js +23 -0
  11. package/dist/components/context-badges.d.ts +4 -1
  12. package/dist/components/context-badges.js +7 -4
  13. package/dist/components/context-bar.d.ts +1 -0
  14. package/dist/components/context-bar.js +11 -3
  15. package/dist/components/data-table/data-table-frame.d.ts +1 -22
  16. package/dist/components/data-table/data-table-frame.js +21 -48
  17. package/dist/components/data-table/data-table-overflow.d.ts +5 -0
  18. package/dist/components/data-table/data-table-overflow.js +42 -0
  19. package/dist/components/data-table/data-table.js +88 -87
  20. package/dist/components/data-table/focus-pane-resize.d.ts +8 -0
  21. package/dist/components/data-table/focus-pane-resize.js +88 -0
  22. package/dist/components/focus-chat-pane.d.ts +8 -0
  23. package/dist/components/focus-chat-pane.js +9 -0
  24. package/dist/components/focus-dialog.d.ts +8 -0
  25. package/dist/components/focus-dialog.js +103 -0
  26. package/dist/components/focus-source-pane.d.ts +7 -0
  27. package/dist/components/focus-source-pane.js +15 -0
  28. package/dist/components/loading-indicator.js +1 -1
  29. package/dist/components/message-timestamp.js +3 -2
  30. package/dist/components/pin-state-icon.d.ts +5 -0
  31. package/dist/components/pin-state-icon.js +7 -0
  32. package/dist/components/pin-toggle.d.ts +7 -0
  33. package/dist/components/pin-toggle.js +10 -0
  34. package/dist/components/sidebar.d.ts +18 -1
  35. package/dist/components/sidebar.js +124 -74
  36. package/dist/components/tool-panel-layout.d.ts +3 -1
  37. package/dist/components/tool-panel-layout.js +5 -5
  38. package/dist/components/tool-panel.d.ts +8 -1
  39. package/dist/components/tool-panel.js +12 -8
  40. package/dist/components/user-menu.js +19 -3
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +2 -0
  43. package/dist/lib/app-store.d.ts +0 -8
  44. package/dist/lib/app-store.js +14 -4
  45. package/dist/lib/chat-context-summary.d.ts +11 -0
  46. package/dist/lib/chat-context-summary.js +37 -0
  47. package/dist/lib/chat-lifecycle-store.d.ts +2 -0
  48. package/dist/lib/chat-lifecycle-store.js +6 -1
  49. package/dist/lib/client-tools.js +7 -0
  50. package/dist/lib/create-ui-id.d.ts +2 -0
  51. package/dist/lib/create-ui-id.js +9 -0
  52. package/dist/lib/focus-context.d.ts +2 -0
  53. package/dist/lib/focus-context.js +78 -0
  54. package/dist/lib/nav-config.d.ts +7 -0
  55. package/dist/lib/tool-panel-store.d.ts +7 -6
  56. package/dist/lib/tool-panel-store.js +29 -3
  57. package/dist/lib/use-new-conversation.js +1 -1
  58. package/dist/workbench/chat-header.d.ts +14 -0
  59. package/dist/workbench/chat-header.js +12 -0
  60. package/dist/workbench/chat-resize-handle.d.ts +4 -0
  61. package/dist/workbench/chat-resize-handle.js +40 -0
  62. package/dist/workbench/content-pane.d.ts +14 -0
  63. package/dist/workbench/content-pane.js +14 -0
  64. package/dist/workbench/conversation-history.d.ts +11 -0
  65. package/dist/workbench/conversation-history.js +83 -0
  66. package/dist/workbench/navigation.d.ts +6 -0
  67. package/dist/workbench/navigation.js +6 -0
  68. package/dist/workbench/selection-bridge.d.ts +5 -0
  69. package/dist/workbench/selection-bridge.js +84 -0
  70. package/dist/workbench/use-chat-width.d.ts +14 -0
  71. package/dist/workbench/use-chat-width.js +56 -0
  72. package/dist/workbench/use-workbench-state.d.ts +50 -0
  73. package/dist/workbench/use-workbench-state.js +145 -0
  74. package/dist/workbench/welcome-intro.d.ts +6 -0
  75. package/dist/workbench/welcome-intro.js +10 -0
  76. package/dist/workbench/workbench-styles.d.ts +1 -0
  77. package/dist/workbench/workbench-styles.js +290 -0
  78. package/dist/workbench/workbench.d.ts +5 -0
  79. package/dist/workbench/workbench.js +32 -0
  80. package/dist/workbench.d.ts +2 -0
  81. package/dist/workbench.js +2 -0
  82. package/package.json +8 -4
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, } from "../index.js";
3
+ import { Button, Input } from "@iloveagents/foundry-web-primitives";
4
+ import { Fragment, useEffect, useState } from "react";
5
+ import { Link } from "react-router";
6
+ import { ArrowLeft, MessageSquare, MoreHorizontal, Pin, Loader2, ChevronDown, Check, } from "lucide-react";
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, }) {
9
+ const [loadingTo, setLoadingTo] = useState(null);
10
+ const [loadError, setLoadError] = useState(null);
11
+ const [all, setAll] = useState(!workspaceId);
12
+ useEffect(() => setAll(!workspaceId), [workspaceId]);
13
+ const [search, setSearch] = useState("");
14
+ const scoped = groups.find((g) => g.meta?.type === "recent-chats" && g.meta?.workspaceId === workspaceId);
15
+ const allItems = scoped?.meta?.allConversations ??
16
+ groups
17
+ .filter((g) => g.meta?.type === "recent-chats" || g.meta?.type === "pinned-chats")
18
+ .flatMap((g) => g.items);
19
+ const scopedItems = all || !workspaceId
20
+ ? allItems
21
+ : (scoped?.meta?.workspaceConversations ?? scoped?.items ?? []);
22
+ const items = [...new Map(scopedItems.map((item) => [item.to, item])).values()]
23
+ .filter((item) => item.meta?.type !== "show-all-chats")
24
+ .filter((item) => item.label.toLocaleLowerCase().includes(search.toLocaleLowerCase()))
25
+ .sort((a, b) => Number(Boolean(b.meta?.pinned)) - Number(Boolean(a.meta?.pinned)));
26
+ const visibleItems = compact ? items.slice(0, 3) : items;
27
+ const pinnedItems = visibleItems.filter((item) => item.meta?.pinned);
28
+ const sections = pinnedItems.length
29
+ ? [
30
+ { label: "Pinned", items: pinnedItems },
31
+ { label: "Recent chats", items: visibleItems.filter((item) => !item.meta?.pinned) },
32
+ ]
33
+ : [{ 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: [
35
+ { value: false, label: "Last used in this workspace" },
36
+ { value: true, label: "All conversations" },
37
+ ].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
38
+ .filter((section) => section.items.length)
39
+ .map((section) => (_jsxs("section", { "aria-label": section.label || "Recent chats", className: "mb-4 last:mb-0", children: [section.label && (_jsx("h3", { "data-conversation-section-label": true, className: "px-2 py-2 text-xs font-medium text-muted-foreground", children: section.label })), section.items.map((item) => item.disabled ? (_jsx("p", { className: "py-3 text-sm text-muted-foreground", children: item.label }, item.to)) : (_jsxs("div", { className: "group flex items-center rounded-lg hover:bg-muted", children: [_jsxs(Link, { to: item.to, onClick: (event) => {
40
+ if (loadingTo) {
41
+ event.preventDefault();
42
+ return;
43
+ }
44
+ if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)
45
+ return;
46
+ {
47
+ const resume = new CustomEvent("preview:resume-chat", {
48
+ cancelable: true,
49
+ detail: {
50
+ to: item.to,
51
+ complete: (error) => {
52
+ setLoadingTo(null);
53
+ if (error)
54
+ setLoadError(error);
55
+ else
56
+ onClose();
57
+ },
58
+ },
59
+ });
60
+ setLoadingTo(item.to);
61
+ setLoadError(null);
62
+ if (!window.dispatchEvent(resume)) {
63
+ event.preventDefault();
64
+ return;
65
+ }
66
+ }
67
+ setLoadingTo(null);
68
+ onClose();
69
+ }, "data-conversation-row": true, className: "flex min-w-0 flex-1 items-center gap-2 rounded-lg px-2 py-1.5 text-sm hover:bg-muted focus-visible:outline-2 focus-visible:outline-primary", children: [loadingTo === item.to ? (_jsx(Loader2, { className: "size-4 shrink-0 animate-spin text-muted-foreground", "aria-label": "Opening chat" })) : item.meta?.pinned ? (_jsx(Pin, { className: "size-4 shrink-0 text-muted-foreground", "aria-label": "Pinned chat" })) : (_jsx(MessageSquare, { className: "size-4 shrink-0 text-muted-foreground" })), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate", children: item.label }), " ", _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: !all &&
70
+ typeof item.meta?.previewEntityName === "string" &&
71
+ item.meta.previewEntityName
72
+ ? `Last at ${item.meta.previewEntityName}`
73
+ : !all
74
+ ? "Last in this workspace"
75
+ : typeof item.meta?.previewSpaceName === "string" &&
76
+ item.meta.previewSpaceName
77
+ ? `Last in ${item.meta.previewSpaceName}`
78
+ : item.meta?.previewContextUnavailable
79
+ ? "Location unavailable"
80
+ : "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
+ 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" }))] })] }));
83
+ }
@@ -0,0 +1,6 @@
1
+ import { type NavGroup } from "../index.js";
2
+ export declare function WorkbenchNavigation({ groups, workspace, onNewConversation, }: {
3
+ groups: NavGroup[];
4
+ workspace: boolean;
5
+ onNewConversation: () => void;
6
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Sidebar, UserMenu, ThemeToggle } from "../index.js";
3
+ import { SelectionBridge } from "./selection-bridge.js";
4
+ export function WorkbenchNavigation({ groups, workspace, onNewConversation, }) {
5
+ return (_jsx(SelectionBridge, { children: _jsx(Sidebar, { groups: groups, collapsedGroupsOnly: true, hideNewConversation: true, newConversationLabel: "New chat", hideFooterActions: workspace, onNewConversation: onNewConversation, footer: _jsx("footer", { className: "shrink-0 border-t border-sidebar-border bg-sidebar px-2 py-2", "aria-label": "Account and appearance", children: _jsxs("div", { className: "flex min-w-0 items-center justify-between gap-1", children: [_jsx(UserMenu, {}), _jsx(ThemeToggle, {})] }) }), collapsedFooter: _jsxs("footer", { className: "preview-account-compact flex shrink-0 flex-col items-center border-t border-sidebar-border py-2", "aria-label": "Account and appearance", children: [_jsx(ThemeToggle, {}), _jsx(UserMenu, {})] }) }) }));
6
+ }
@@ -0,0 +1,5 @@
1
+ import { type ReactNode } from "react";
2
+ /** One continuous contour connects the selected row to the chat boundary. */
3
+ export declare function SelectionBridge({ children }: {
4
+ children: ReactNode;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,84 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ /** One continuous contour connects the selected row to the chat boundary. */
4
+ export function SelectionBridge({ children }) {
5
+ const root = useRef(null);
6
+ const [anchor, setAnchor] = useState(null);
7
+ useEffect(() => {
8
+ const node = root.current;
9
+ if (!node)
10
+ return;
11
+ let frame = 0;
12
+ const measure = () => {
13
+ cancelAnimationFrame(frame);
14
+ frame = requestAnimationFrame(() => {
15
+ const bounds = node.getBoundingClientRect();
16
+ const row = node.querySelector('[data-sidebar-surface="desktop"] [data-active-nav-row="true"]');
17
+ const rect = row?.getBoundingClientRect();
18
+ const scroll = row?.closest("nav")?.getBoundingClientRect();
19
+ const visible = rect &&
20
+ bounds.width > 80 &&
21
+ rect.height > 0 &&
22
+ (!scroll || (rect.top >= scroll.top && rect.bottom <= scroll.bottom));
23
+ setAnchor(visible
24
+ ? {
25
+ top: rect.top - bounds.top,
26
+ height: rect.height,
27
+ left: rect.left - bounds.left + 0.5,
28
+ rowRight: rect.right - bounds.left - 1,
29
+ width: bounds.width,
30
+ navHeight: bounds.height,
31
+ surfaceWidth: node.nextElementSibling?.getBoundingClientRect().width ?? 0,
32
+ }
33
+ : null);
34
+ });
35
+ };
36
+ const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
37
+ const mutation = typeof MutationObserver === "undefined" ? null : new MutationObserver(measure);
38
+ resize?.observe(node);
39
+ if (node.nextElementSibling)
40
+ resize?.observe(node.nextElementSibling);
41
+ if (node.parentElement)
42
+ resize?.observe(node.parentElement);
43
+ mutation?.observe(node, {
44
+ subtree: true,
45
+ childList: true,
46
+ attributes: true,
47
+ attributeFilter: ["class", "style", "data-active-nav-row"],
48
+ });
49
+ node.addEventListener("scroll", measure, true);
50
+ measure();
51
+ return () => {
52
+ cancelAnimationFrame(frame);
53
+ resize?.disconnect();
54
+ mutation?.disconnect();
55
+ node.removeEventListener("scroll", measure, true);
56
+ };
57
+ }, []);
58
+ return (_jsxs("div", { ref: root, "data-connected-navigation": anchor ? "true" : undefined, className: "relative flex shrink-0", children: [_jsx("style", { children: `
59
+ [data-connected-navigation="true"] + [data-workbench-surface]::after { display: none; }
60
+ .workbench-preview [data-sidebar-surface="desktop"] nav { padding-right: 16px; }
61
+ .workbench-preview [data-sidebar-surface="desktop"] { border-right-color: transparent; }
62
+ [data-connected-navigation="true"] [data-sidebar-surface="desktop"] [data-active-nav-row="true"] {
63
+ background: var(--background);
64
+ border-color: transparent; border-top-right-radius: 0; border-bottom-right-radius: 0;
65
+ }
66
+ ` }), children, anchor && (_jsxs("svg", { "aria-hidden": "true", className: "pointer-events-none absolute z-20", style: { inset: 0, width: anchor.width + anchor.surfaceWidth, height: anchor.navHeight }, viewBox: `0 0 ${anchor.width + anchor.surfaceWidth} ${anchor.navHeight}`, children: [_jsx("path", { fill: "var(--background)", d: `M ${anchor.width},${anchor.top - 7} Q ${anchor.width},${anchor.top} ${anchor.width - 8},${anchor.top}
67
+ H ${anchor.rowRight} V ${anchor.top + anchor.height} H ${anchor.width - 8}
68
+ Q ${anchor.width},${anchor.top + anchor.height} ${anchor.width},${anchor.top + anchor.height + 7} Z` }), _jsx("path", { fill: "none", stroke: "var(--sidebar-ring)", strokeOpacity: 0.28, strokeWidth: 1, d: `M ${anchor.width + 12},0.5
69
+ H ${anchor.width + anchor.surfaceWidth - 12}
70
+ Q ${anchor.width + anchor.surfaceWidth - 0.5},0.5 ${anchor.width + anchor.surfaceWidth - 0.5},12
71
+ V ${anchor.navHeight - 12}
72
+ Q ${anchor.width + anchor.surfaceWidth - 0.5},${anchor.navHeight - 0.5} ${anchor.width + anchor.surfaceWidth - 12},${anchor.navHeight - 0.5}
73
+ H ${anchor.width + 12}
74
+ Q ${anchor.width - 0.5},${anchor.navHeight - 0.5} ${anchor.width - 0.5},${anchor.navHeight - 12}
75
+ V ${anchor.top + anchor.height + 7}
76
+ Q ${anchor.width - 0.5},${anchor.top + anchor.height - 0.5} ${anchor.width - 8},${anchor.top + anchor.height - 0.5}
77
+ H ${anchor.left + 12}
78
+ Q ${anchor.left},${anchor.top + anchor.height - 0.5} ${anchor.left},${anchor.top + anchor.height - 12}
79
+ V ${anchor.top + 12}
80
+ Q ${anchor.left},${anchor.top + 0.5} ${anchor.left + 12},${anchor.top + 0.5}
81
+ H ${anchor.width - 8}
82
+ Q ${anchor.width - 0.5},${anchor.top + 0.5} ${anchor.width - 0.5},${anchor.top - 7}
83
+ V 12 Q ${anchor.width - 0.5},0.5 ${anchor.width + 12},0.5 Z` })] }))] }));
84
+ }
@@ -0,0 +1,14 @@
1
+ export declare function chatWidthLimits(available: number): {
2
+ min: number;
3
+ max: number;
4
+ defaultWidth: number;
5
+ };
6
+ export declare function useChatWidth(): {
7
+ min: number;
8
+ max: number;
9
+ defaultWidth: number;
10
+ containerRef: import("react").RefObject<HTMLDivElement | null>;
11
+ width: number;
12
+ setWidth: (value: number | null) => void;
13
+ compact: boolean;
14
+ };
@@ -0,0 +1,56 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ const WIDTH_KEY = "foundry.workbench-preview.chat-width";
3
+ const MIN_WIDTH = 360;
4
+ const CONTENT_MIN = 440;
5
+ export function chatWidthLimits(available) {
6
+ const max = Math.max(MIN_WIDTH, Math.min(640, available - CONTENT_MIN));
7
+ return {
8
+ min: MIN_WIDTH,
9
+ max,
10
+ defaultWidth: Math.min(max, Math.max(MIN_WIDTH, Math.min(420, Math.round(available * 0.36)))),
11
+ };
12
+ }
13
+ export function useChatWidth() {
14
+ const containerRef = useRef(null);
15
+ const [available, setAvailable] = useState(1000);
16
+ const [preferred, setPreferred] = useState(() => {
17
+ try {
18
+ const stored = Number(localStorage.getItem(WIDTH_KEY));
19
+ return Number.isFinite(stored) && stored >= MIN_WIDTH ? Math.min(stored, 640) : null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ });
25
+ useEffect(() => {
26
+ const node = containerRef.current;
27
+ if (!node)
28
+ return;
29
+ const measure = () => {
30
+ if (node.clientWidth > 0)
31
+ setAvailable(node.clientWidth);
32
+ };
33
+ measure();
34
+ if (typeof ResizeObserver === "undefined")
35
+ return;
36
+ const observer = new ResizeObserver(measure);
37
+ observer.observe(node);
38
+ return () => observer.disconnect();
39
+ }, []);
40
+ const limits = chatWidthLimits(available);
41
+ const width = Math.min(limits.max, Math.max(limits.min, preferred ?? limits.defaultWidth));
42
+ const setWidth = (value) => {
43
+ const next = value === null ? null : Math.max(limits.min, Math.min(limits.max, value));
44
+ setPreferred(next);
45
+ try {
46
+ if (next === null)
47
+ localStorage.removeItem(WIDTH_KEY);
48
+ else
49
+ localStorage.setItem(WIDTH_KEY, String(next));
50
+ }
51
+ catch {
52
+ /* Storage may be disabled. */
53
+ }
54
+ };
55
+ return { containerRef, width, setWidth, compact: available < MIN_WIDTH + CONTENT_MIN, ...limits };
56
+ }
@@ -0,0 +1,50 @@
1
+ export declare function useWorkbenchState(chatRoute: boolean): {
2
+ panelOpen: boolean;
3
+ chatOnly: boolean;
4
+ pageLabel: string;
5
+ chatWidth: {
6
+ min: number;
7
+ max: number;
8
+ defaultWidth: number;
9
+ containerRef: import("react").RefObject<HTMLDivElement | null>;
10
+ width: number;
11
+ setWidth: (value: number | null) => void;
12
+ compact: boolean;
13
+ };
14
+ collapsed: boolean;
15
+ setCollapsed: import("react").Dispatch<import("react").SetStateAction<boolean>>;
16
+ compactPane: "content" | "chat";
17
+ setCompactPane: import("react").Dispatch<import("react").SetStateAction<"content" | "chat">>;
18
+ contentCollapsed: boolean;
19
+ setContentCollapsed: import("react").Dispatch<import("react").SetStateAction<boolean>>;
20
+ contentHidden: boolean;
21
+ historyOpen: boolean;
22
+ emptyChat: boolean;
23
+ showHistory: boolean;
24
+ spaciousChat: boolean;
25
+ returnToChat: () => void;
26
+ openHistory: () => void;
27
+ historyShortcut: import("react/jsx-runtime").JSX.Element;
28
+ toggleNavigation: () => void;
29
+ navGroups: import("../index.js").NavGroup[];
30
+ workspaceGroups: import("../index.js").NavGroup[] | undefined;
31
+ staticNavigationGroups: {
32
+ collapsible: boolean;
33
+ label: string;
34
+ tree?: boolean;
35
+ icon?: import("lucide-react").LucideIcon;
36
+ items: import("../index.js").NavItem[];
37
+ priority?: number;
38
+ defaultCollapsed?: boolean;
39
+ to?: string;
40
+ hideLabel?: boolean;
41
+ focusOn?: string;
42
+ description?: string;
43
+ meta?: Record<string, unknown>;
44
+ contextInstructions?: string;
45
+ createActions?: import("../index.js").NavGroupCreateAction[];
46
+ collapsedRail?: (ctx: import("../index.js").CollapsedRailContext) => import("../index.js").CollapsedRailItem[];
47
+ }[];
48
+ closePanel: () => void;
49
+ startChat: () => void;
50
+ };
@@ -0,0 +1,145 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useLocation } from "react-router";
3
+ import { useAuiState } from "@assistant-ui/react";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { Button } from "@iloveagents/foundry-web-primitives";
6
+ import { useSidebarStore, useChatBubbleStore, useToolPanelStore, useNewConversation, useNavStore, useAppStore, } from "../index.js";
7
+ import { History, Settings, Layers } from "lucide-react";
8
+ import { useChatWidth } from "./use-chat-width.js";
9
+ function useWorkbenchNavigation() {
10
+ const navGroups = useNavStore((s) => s.config);
11
+ const workspaceGroups = useMemo(() => {
12
+ const workspace = navGroups.find((group) => group.meta?.type === "spaces-workspaces" && group.meta?.exclusiveRootId);
13
+ if (!workspace)
14
+ return undefined;
15
+ return [workspace];
16
+ }, [navGroups]);
17
+ const navigationGroups = useMemo(() => {
18
+ if (workspaceGroups) {
19
+ const space = workspaceGroups[0];
20
+ const root = space.items.find((item) => item.to === space.focusOn) ?? space.items[0];
21
+ return [
22
+ {
23
+ ...space,
24
+ label: "Spaces",
25
+ icon: Layers,
26
+ priority: 1000,
27
+ items: root
28
+ ? [{ ...root, children: root.childrenHidden ? root.children : undefined }]
29
+ : space.items,
30
+ },
31
+ {
32
+ label: "Content",
33
+ tree: true,
34
+ hideLabel: true,
35
+ priority: 800,
36
+ items: root?.childrenHidden ? [] : (root?.children ?? []),
37
+ meta: { hideInCollapsedRail: true },
38
+ },
39
+ ];
40
+ }
41
+ return [
42
+ ...navGroups
43
+ .filter((group) => !["recent-chats", "pinned-chats"].includes(String(group.meta?.type)))
44
+ .map((group) => group.meta?.type === "spaces-workspaces"
45
+ ? { ...group, label: "Spaces", icon: Layers }
46
+ : group.label === "Admin"
47
+ ? { ...group, icon: Settings }
48
+ : group.label === "Activity"
49
+ ? { ...group, priority: 1100 }
50
+ : group),
51
+ ];
52
+ }, [workspaceGroups, navGroups]);
53
+ const staticNavigationGroups = useMemo(() => navigationGroups.map((group) => ({ ...group, collapsible: false })), [navigationGroups]);
54
+ return { navGroups, workspaceGroups, staticNavigationGroups };
55
+ }
56
+ export function useWorkbenchState(chatRoute) {
57
+ const panelOpen = useToolPanelStore((s) => s.isOpen && !s.isWindowHosted);
58
+ const chatOnly = chatRoute && !panelOpen;
59
+ const location = useLocation();
60
+ const pageLabel = useAppStore((s) => s.navContext.label);
61
+ const chatWidth = useChatWidth();
62
+ const [desktopCollapsed, setCollapsed] = useState(false);
63
+ const collapsed = !chatWidth.compact && desktopCollapsed;
64
+ const [compactPane, setCompactPane] = useState(chatOnly ? "chat" : "content");
65
+ const [contentCollapsed, setContentCollapsed] = useState(false);
66
+ const contentHidden = chatOnly || (chatWidth.compact ? compactPane !== "content" : contentCollapsed);
67
+ useEffect(() => {
68
+ setContentCollapsed(chatOnly);
69
+ setCompactPane(chatOnly ? "chat" : "content");
70
+ if (chatOnly)
71
+ setCollapsed(false);
72
+ // Navigation is an explicit request to see content, including selecting
73
+ // the same address again. Collapsing remains local until the next entry.
74
+ }, [chatOnly, location.key]);
75
+ const [historyOpen, setHistoryOpen] = useState(false);
76
+ const emptyChat = useAuiState((s) => s.thread.messages.length === 0 && !s.thread.isRunning && !s.thread.isLoading);
77
+ const wasEmptyChat = useRef(emptyChat);
78
+ useEffect(() => {
79
+ if (wasEmptyChat.current && !emptyChat)
80
+ setHistoryOpen(false);
81
+ wasEmptyChat.current = emptyChat;
82
+ }, [emptyChat]);
83
+ const showHistory = historyOpen;
84
+ const spaciousChat = chatOnly || contentCollapsed;
85
+ const returnToChat = () => {
86
+ setHistoryOpen(false);
87
+ };
88
+ const openHistory = () => {
89
+ setHistoryOpen(true);
90
+ };
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"] }));
92
+ const toggleNavigation = useSidebarStore((s) => s.toggleMobile);
93
+ const { navGroups, workspaceGroups, staticNavigationGroups } = useWorkbenchNavigation();
94
+ useEffect(() => {
95
+ if (panelOpen) {
96
+ setContentCollapsed(false);
97
+ setCompactPane("content");
98
+ }
99
+ }, [panelOpen]);
100
+ const closePanel = useToolPanelStore((s) => s.closePanel);
101
+ const requestedChat = useChatBubbleStore((s) => s.isOpen || s.isExpanded);
102
+ const newConversation = useNewConversation({
103
+ navigateToChat: chatRoute,
104
+ preserveNavigation: !chatRoute,
105
+ });
106
+ const startChat = () => {
107
+ setHistoryOpen(false);
108
+ setCompactPane("chat");
109
+ newConversation();
110
+ };
111
+ useEffect(() => {
112
+ if (requestedChat) {
113
+ setHistoryOpen(false);
114
+ setCollapsed(false);
115
+ setCompactPane("chat");
116
+ useChatBubbleStore.getState().close();
117
+ }
118
+ }, [requestedChat]);
119
+ return {
120
+ panelOpen,
121
+ chatOnly,
122
+ pageLabel,
123
+ chatWidth,
124
+ collapsed,
125
+ setCollapsed,
126
+ compactPane,
127
+ setCompactPane,
128
+ contentCollapsed,
129
+ setContentCollapsed,
130
+ contentHidden,
131
+ historyOpen,
132
+ emptyChat,
133
+ showHistory,
134
+ spaciousChat,
135
+ returnToChat,
136
+ openHistory,
137
+ historyShortcut,
138
+ toggleNavigation,
139
+ navGroups,
140
+ workspaceGroups,
141
+ staticNavigationGroups,
142
+ closePanel,
143
+ startChat,
144
+ };
145
+ }
@@ -0,0 +1,6 @@
1
+ import { type ReactNode } from "react";
2
+ import { useNavStore } from "../index.js";
3
+ export declare function WelcomeIntro({ navGroups, historyShortcut, }: {
4
+ navGroups: ReturnType<typeof useNavStore.getState>["config"];
5
+ historyShortcut: ReactNode;
6
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from "@iloveagents/foundry-web-primitives";
3
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, } from "../index.js";
4
+ import { Layers } from "lucide-react";
5
+ 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
+ .filter((group) => group.meta?.type === "spaces-workspaces")
8
+ .flatMap((group) => group.createActions ?? [])
9
+ .map((action) => (_jsx(DropdownMenuItem, { onSelect: () => action.handler(), children: action.label }, action.id))) })] }) }))] })] }));
10
+ }
@@ -0,0 +1 @@
1
+ export declare function WorkbenchStyles(): import("react/jsx-runtime").JSX.Element;