@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
@@ -9,10 +9,11 @@ import { cn } from "@iloveagents/foundry-web-primitives";
9
9
  */
10
10
  export const MessageTimestamp = ({ className }) => {
11
11
  const createdAt = useAuiState((s) => s.message.createdAt);
12
- if (!createdAt)
12
+ const running = useAuiState((s) => s.thread.isRunning);
13
+ if (!createdAt || running)
13
14
  return null;
14
15
  const d = createdAt instanceof Date ? createdAt : new Date(createdAt);
15
16
  if (Number.isNaN(d.getTime()))
16
17
  return null;
17
- return (_jsx("span", { title: d.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }), className: cn("text-xs text-muted-foreground/70 tabular-nums select-none", "opacity-0 transition-opacity group-hover:opacity-100", className), children: d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }) }));
18
+ return (_jsx("span", { title: d.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }), className: cn("text-xs text-muted-foreground/70 tabular-nums select-none", "opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100", className), children: d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }) }));
18
19
  };
@@ -0,0 +1,5 @@
1
+ /** State glyph; controls retain explicit Pin/Unpin action labels. */
2
+ export declare function PinStateIcon({ pinned, className }: {
3
+ pinned: boolean;
4
+ className?: string;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Pin } from "lucide-react";
3
+ import { cn } from "@iloveagents/foundry-web-primitives";
4
+ /** State glyph; controls retain explicit Pin/Unpin action labels. */
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) }));
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { ComponentProps } from "react";
2
+ import { Button } from "@iloveagents/foundry-web-primitives";
3
+ /** Pin state uses one upright glyph everywhere; the label describes the action. */
4
+ export declare function PinToggle({ pinned, label, className, ...props }: Omit<ComponentProps<typeof Button>, "children"> & {
5
+ pinned: boolean;
6
+ label: string;
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PinStateIcon } from "./pin-state-icon.js";
3
+ import { Button, cn } from "@iloveagents/foundry-web-primitives";
4
+ /** Pin state uses one upright glyph everywhere; the label describes the action. */
5
+ export function PinToggle({ pinned, label, className, ...props }) {
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 }) }));
10
+ }
@@ -1,4 +1,21 @@
1
+ import { type ReactNode } from "react";
2
+ import type { NavGroup } from "../lib/nav-config.js";
1
3
  declare function scrollNavRowIntoView(nav: HTMLElement, row: HTMLElement): void;
2
4
  export declare const _scrollNavRowIntoViewForTest: typeof scrollNavRowIntoView;
3
- export declare function Sidebar(): import("react/jsx-runtime").JSX.Element;
5
+ interface SidebarViewProps {
6
+ /** Display-only navigation override; the navigation registry remains intact. */
7
+ groups?: NavGroup[];
8
+ onNewConversation?: () => void;
9
+ hideFooterActions?: boolean;
10
+ hideNewConversation?: boolean;
11
+ /** Compact rail shows group entry points instead of every child item. */
12
+ collapsedGroupsOnly?: boolean;
13
+ /** Place the primary action beneath a scope heading instead of above navigation. */
14
+ newConversationAfterGroup?: string;
15
+ newConversationLabel?: string;
16
+ }
17
+ export declare function Sidebar({ footer, collapsedFooter, ...view }?: SidebarViewProps & {
18
+ footer?: ReactNode;
19
+ collapsedFooter?: ReactNode;
20
+ }): import("react/jsx-runtime").JSX.Element;
4
21
  export {};
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
+ import { Fragment, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
3
3
  import { NavLink, useLocation, useNavigate } from "react-router";
4
- import { Ellipsis, PanelLeft, PanelLeftOpen, SquarePen, ChevronDown, ChevronRight, GripVertical, Plus, Upload, ChevronLeft, } from "lucide-react";
4
+ import { Ellipsis, Layers, PanelLeft, PanelLeftOpen, SquarePen, ChevronDown, ChevronRight, GripVertical, Plus, Upload, ChevronLeft, } from "lucide-react";
5
5
  import { cn } from "@iloveagents/foundry-web-primitives";
6
- import { useSidebarStore, RAIL_WIDTH, SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH, } from "../lib/sidebar-store.js";
6
+ import { useSidebarStore, RAIL_WIDTH, SIDEBAR_WIDTH, SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH, } from "../lib/sidebar-store.js";
7
7
  import { useToolPanelStore } from "../lib/tool-panel-store.js";
8
8
  import { useNewConversation } from "../lib/use-new-conversation.js";
9
9
  import { useNavStore } from "../lib/nav-store.js";
@@ -405,11 +405,11 @@ function ContainerDropZone({ dnd }) {
405
405
  return;
406
406
  }
407
407
  await dnd.onDrop(draggedData);
408
- }, className: cn("mx-2 rounded-md transition-all overflow-hidden", isDragOver
408
+ }, className: cn("relative mx-2 rounded-md overflow-hidden", isDragOver
409
409
  ? cn("h-7 flex items-center justify-center gap-1", isFileDragOver
410
410
  ? "bg-primary/10 border border-dashed border-primary"
411
411
  : "bg-primary/10 border border-dashed border-primary/30")
412
- : "h-1"), children: isDragOver && (_jsxs(_Fragment, { children: [isFileDragOver && _jsx(Upload, { className: "size-3 text-primary" }), _jsx("span", { className: "text-[10px] text-primary/70", children: getContainerDropZoneLabel(isFileDragOver) })] })) }));
412
+ : "-mt-1 h-1"), children: isDragOver && (_jsxs(_Fragment, { children: [isFileDragOver && _jsx(Upload, { className: "size-3 text-primary" }), _jsx("span", { className: "text-[10px] text-primary/70", children: getContainerDropZoneLabel(isFileDragOver) })] })) }));
413
413
  }
414
414
  /**
415
415
  * Leading disclosure cell on every nav row — the Confluence page-tree
@@ -437,7 +437,7 @@ function ContainerDropZone({ dnd }) {
437
437
  function NavTwisty({ hasChildren, childrenUnloaded = false, isExpanded, onToggle, label, isActive, depth, inTree = true, }) {
438
438
  const cell = cn(NAV_TWISTY_CLASS, getNavDepthOffsets(depth).column);
439
439
  if (!hasChildren) {
440
- return (_jsx("span", { "data-nav-twisty-cell": true, "aria-hidden": "true", className: cn(cell, "pointer-events-none"), children: inTree && !childrenUnloaded && (_jsx("span", { "data-nav-leaf-dot": true, className: cn("size-1 rounded-full", isActive ? "bg-primary/70" : "bg-sidebar-foreground/30") })) }));
440
+ return (_jsx("span", { "data-nav-twisty-cell": true, "aria-hidden": "true", className: cn(cell, "pointer-events-none"), children: inTree && !childrenUnloaded && (_jsx("span", { "data-nav-leaf-dot": true, className: cn("block size-1 shrink-0 rounded-full", isActive ? "bg-primary/70" : "bg-sidebar-foreground/30") })) }));
441
441
  }
442
442
  return (_jsx("span", { "data-nav-twisty-cell": true, className: cell, children: _jsx("button", { type: "button", "data-nav-twisty": true, "aria-expanded": isExpanded, "aria-label": isExpanded ? `Collapse ${label}` : `Expand ${label}`, onClick: (e) => {
443
443
  e.preventDefault();
@@ -478,7 +478,7 @@ function NavLeafItem({ item, isActive, onClick, depth = 0, }) {
478
478
  const activeButtonRef = useActiveNavRowScroll(isActive && !hasActions);
479
479
  // Items with actions use the group layout (button + action controls)
480
480
  if (hasActions) {
481
- return (_jsxs("div", { ref: activeRowRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, ...(isDisabled ? {} : dropTargetProps), ...(isDisabled ? {} : draggableProps), className: cn("group relative flex items-center w-full rounded-xl border border-transparent transition-colors", isDisabled
481
+ return (_jsxs("div", { ref: activeRowRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, style: { "--nav-depth": depth }, ...(isDisabled ? {} : dropTargetProps), ...(isDisabled ? {} : draggableProps), className: cn("group relative flex items-center w-full rounded-xl border border-transparent transition-colors", isDisabled
482
482
  ? "opacity-40"
483
483
  : isActive
484
484
  ? ACTIVE_NAV_ROW_CLASS
@@ -495,7 +495,7 @@ function NavLeafItem({ item, isActive, onClick, depth = 0, }) {
495
495
  ? ACTIVE_NAV_BADGE_CLASS
496
496
  : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }));
497
497
  }
498
- return (_jsxs("button", { ref: activeButtonRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, ...(isDisabled ? {} : draggableProps), ...(isDisabled ? {} : dropTargetProps), className: cn("relative flex items-center gap-2 w-full", "rounded-lg border border-transparent pr-3 py-1 text-sm text-left", bodyPl, isDisabled
498
+ return (_jsxs("button", { ref: activeButtonRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, style: { "--nav-depth": depth }, type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, ...(isDisabled ? {} : draggableProps), ...(isDisabled ? {} : dropTargetProps), className: cn("relative flex items-center gap-2 w-full", "rounded-lg border border-transparent pr-3 py-1 text-sm text-left", bodyPl, isDisabled
499
499
  ? "opacity-40 cursor-default"
500
500
  : "text-sidebar-foreground/80 transition-colors hover:bg-sidebar-accent/70", !isDisabled && isActive && ACTIVE_NAV_ROW_CLASS, isDragOver &&
501
501
  !isFileDragOver &&
@@ -515,7 +515,7 @@ function SubNavItems({ items, parentPath, depth = 1, }) {
515
515
  // children sharing the parent path (e.g., Documents sub-filters) use label matching.
516
516
  const allShareParent = items.every((c) => c.to === parentPath);
517
517
  return (_jsx("div", { children: items.map((child) => {
518
- const hasNestedChildren = child.children && child.children.length > 0;
518
+ const hasNestedChildren = !child.childrenHidden && child.children && child.children.length > 0;
519
519
  // Folders with children render as collapsible nested items
520
520
  if (hasNestedChildren) {
521
521
  return _jsx(NestedFolderItem, { item: child, depth: depth }, child.to + child.label);
@@ -525,7 +525,7 @@ function SubNavItems({ items, parentPath, depth = 1, }) {
525
525
  // parent path (e.g., folders) are never highlighted on the parent page.
526
526
  const isActive = allShareParent
527
527
  ? isOnParent && activeLabel === child.label
528
- : child.to !== parentPath && currentPath === child.to;
528
+ : child.to !== parentPath && isSelectedNavItem(currentPath, child);
529
529
  return (_jsx(NavLeafItem, { item: child, isActive: isActive, depth: depth, onClick: () => {
530
530
  setActiveLabel(child.label);
531
531
  setNavContext({
@@ -547,6 +547,11 @@ function isInSubtree(currentPage, item) {
547
547
  return true;
548
548
  return item.children?.some((c) => isInSubtree(currentPage, c)) ?? false;
549
549
  }
550
+ /** A list owns the sidebar selection of descendants it deliberately does not draw.
551
+ * The route lookup still resolves the actual descendant for chat and page actions. */
552
+ function isSelectedNavItem(currentPage, item) {
553
+ return currentPage === item.to || Boolean(item.childrenHidden && isInSubtree(currentPage, item));
554
+ }
550
555
  /** Nested folder with collapsible children (used inside SubNavItems). */
551
556
  function NestedFolderItem({ item, depth }) {
552
557
  const navigate = useNavigate();
@@ -554,16 +559,15 @@ function NestedFolderItem({ item, depth }) {
554
559
  const setNavContext = useAppStore((s) => s.setNavContext);
555
560
  const navContext = useAppStore((s) => s.navContext);
556
561
  const isInsideSubtree = isInSubtree(currentPath, item);
557
- const isActive = currentPath === item.to;
562
+ const isActive = isSelectedNavItem(currentPath, item);
558
563
  const isAncestorActive = isInsideSubtree && !isActive;
559
- const [manualOverride, setManualOverride] = useState(null);
560
- const isExpanded = manualOverride ?? isInsideSubtree;
564
+ const [isExpanded, setExpanded] = useTreeExpanded(item.to, isInsideSubtree);
561
565
  const { draggableProps, dropTargetProps, isDragOver, isFileDragOver } = useNavItemDnd(item.dnd);
562
566
  const bodyPl = getNavDepthOffsets(depth).body;
563
567
  const isDisabled = item.disabled === true;
564
568
  const hasActions = Boolean(!isDisabled && item.actions && item.actions.length > 0);
565
569
  const activeRowRef = useActiveNavRowScroll(isActive);
566
- return (_jsxs("div", { ...(isDisabled ? {} : dropTargetProps), children: [_jsxs("div", { ref: activeRowRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, ...(isDisabled ? {} : draggableProps), className: cn("group relative flex items-center w-full", "rounded-xl border border-transparent transition-colors", isDisabled
570
+ return (_jsxs("div", { ...(isDisabled ? {} : dropTargetProps), children: [_jsxs("div", { ref: activeRowRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, style: { "--nav-depth": depth }, ...(isDisabled ? {} : draggableProps), className: cn("group relative flex items-center w-full", "rounded-xl border border-transparent transition-colors", isDisabled
567
571
  ? "opacity-40"
568
572
  : isActive
569
573
  ? ACTIVE_NAV_ROW_CLASS
@@ -572,10 +576,10 @@ function NestedFolderItem({ item, depth }) {
572
576
  !isDisabled &&
573
577
  "ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
574
578
  !isDisabled &&
575
- "outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: true, isExpanded: isExpanded, onToggle: () => setManualOverride(!isExpanded), label: item.label, isActive: (isActive || isAncestorActive) && !isDisabled, depth: depth }), _jsxs("button", { type: "button", disabled: isDisabled, onClick: () => {
579
+ "outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: true, isExpanded: isExpanded, onToggle: () => setExpanded(!isExpanded), label: item.label, isActive: (isActive || isAncestorActive) && !isDisabled, depth: depth }), _jsxs("button", { type: "button", disabled: isDisabled, onClick: () => {
576
580
  if (isDisabled)
577
581
  return;
578
- setManualOverride(true);
582
+ setExpanded(true);
579
583
  setNavContext({
580
584
  ...navContext,
581
585
  label: item.label,
@@ -599,11 +603,11 @@ function NestedFolderItem({ item, depth }) {
599
603
  : "text-sidebar-icon") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
600
604
  ? ACTIVE_NAV_BADGE_CLASS
601
605
  : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }), isExpanded && item.children && (_jsxs("div", { children: [item.children.map((child) => {
602
- const hasNestedChildren = child.children && child.children.length > 0;
606
+ const hasNestedChildren = !child.childrenHidden && child.children && child.children.length > 0;
603
607
  if (hasNestedChildren) {
604
608
  return (_jsx(NestedFolderItem, { item: child, depth: depth + 1 }, child.to + child.label));
605
609
  }
606
- const isChildActive = currentPath === child.to;
610
+ const isChildActive = isSelectedNavItem(currentPath, child);
607
611
  return (_jsx(NavLeafItem, { item: child, isActive: isChildActive, depth: depth + 1, onClick: () => {
608
612
  setNavContext({
609
613
  ...navContext,
@@ -624,13 +628,13 @@ function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader
624
628
  const setNavContext = useAppStore((s) => s.setNavContext);
625
629
  const navContext = useAppStore((s) => s.navContext);
626
630
  const isInsideSubtree = isInSubtree(currentPath, item);
627
- const isActive = currentPath === item.to;
631
+ const isActive = isSelectedNavItem(currentPath, item);
628
632
  const isAncestorActive = isInsideSubtree && !isActive;
629
- const [manualOverride, setManualOverride] = useState(null);
633
+ const [treeExpanded, setExpanded] = useTreeExpanded(item.to, isInsideSubtree);
630
634
  // A heading is never collapsed and carries no chevron: its children ARE
631
635
  // the tree it stands for.
632
- const isExpanded = heading ? true : (manualOverride ?? isInsideSubtree);
633
- const hasChildren = Boolean(item.children && item.children.length > 0);
636
+ const isExpanded = heading ? true : treeExpanded;
637
+ const hasChildren = Boolean(!item.childrenHidden && item.children && item.children.length > 0);
634
638
  const isDisabled = item.disabled === true;
635
639
  const hasActions = Boolean(!isDisabled && item.actions && item.actions.length > 0);
636
640
  const rowInTree = inTree && !heading;
@@ -654,7 +658,7 @@ function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader
654
658
  !isDisabled &&
655
659
  "ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
656
660
  !isDisabled &&
657
- "outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: hasChildren && !heading, childrenUnloaded: item.childrenUnloaded, inTree: rowInTree, isExpanded: isExpanded, onToggle: hasChildren && !heading ? () => setManualOverride(!isExpanded) : undefined, label: item.label, isActive: (selected || ancestor) && !isDisabled, depth: 0 }), _jsxs("button", { type: "button", onClick: () => {
661
+ "outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: hasChildren && !heading, childrenUnloaded: item.childrenUnloaded, inTree: rowInTree, isExpanded: isExpanded, onToggle: hasChildren && !heading ? () => setExpanded(!isExpanded) : undefined, label: item.label, isActive: (selected || ancestor) && !isDisabled, depth: 0 }), _jsxs("button", { type: "button", onClick: () => {
658
662
  if (isDisabled)
659
663
  return;
660
664
  setNavContext({
@@ -667,7 +671,7 @@ function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader
667
671
  .join("\n") || null,
668
672
  });
669
673
  if (hasChildren)
670
- setManualOverride(true);
674
+ setExpanded(true);
671
675
  navigate(item.to);
672
676
  }, className: cn("flex items-center gap-2 flex-1 min-w-0 py-1.5 text-sm text-left", bodyClass, "pr-3 transition-[padding] duration-150", hasActions && NAV_ROW_ACTIONS_CLEARANCE, isDisabled
673
677
  ? "text-sidebar-foreground/45 cursor-default"
@@ -689,7 +693,7 @@ function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader
689
693
  // Transparent border for box-model parity — see the NavLink variant.
690
694
  "rounded-lg border border-transparent pr-3 py-1.5 text-left text-sm text-sidebar-foreground/45"), children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, inTree: rowInTree, label: item.label, depth: 0 }), _jsx(item.icon, { className: "size-4 shrink-0 text-sidebar-foreground/45" }), _jsx("span", { className: "truncate", children: item.label })] }));
691
695
  }
692
- return (_jsx(NavLink, { ref: activeLinkRef, "data-active-nav-row": currentPath === item.to ? "true" : undefined, "data-nav-depth": 0, to: item.to, ...draggableProps, ...dropTargetProps, className: ({ isActive }) => cn("relative flex items-center gap-2 w-full", bodyClass,
696
+ return (_jsx(NavLink, { ref: activeLinkRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": 0, to: item.to, ...draggableProps, ...dropTargetProps, className: () => cn("relative flex items-center gap-2 w-full", bodyClass,
693
697
  // The transparent border is NOT decoration: it is what makes this
694
698
  // row's box model match every other variant. An absolutely
695
699
  // positioned twisty resolves against the PADDING box, so a row
@@ -698,7 +702,7 @@ function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader
698
702
  "rounded-xl border border-transparent pr-3 py-1.5 text-sm text-left", "text-sidebar-foreground/85 transition-colors hover:bg-sidebar-accent/70", headingClass, isActive && cn(ACTIVE_NAV_ROW_CLASS, ACTIVE_NAV_TEXT_CLASS), isDragOver &&
699
703
  !isFileDragOver &&
700
704
  "bg-sidebar-selected ring-2 ring-inset ring-sidebar-ring/50", isFileDragOver &&
701
- "outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && "opacity-50"), children: ({ isActive }) => (_jsxs(_Fragment, { children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, inTree: rowInTree, label: item.label, isActive: isActive, depth: 0 }), _jsx(item.icon, { className: cn("size-4 shrink-0", isActive ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-icon") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
705
+ "outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && "opacity-50"), children: () => (_jsxs(_Fragment, { children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, inTree: rowInTree, label: item.label, isActive: isActive, depth: 0 }), _jsx(item.icon, { className: cn("size-4 shrink-0", isActive ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-icon") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
702
706
  ? ACTIVE_NAV_BADGE_CLASS
703
707
  : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot }), isFileDragOver && (_jsx(Upload, { "aria-hidden": "true", className: "ml-auto size-3.5 shrink-0 text-primary pointer-events-none" }))] })) }));
704
708
  }
@@ -729,7 +733,7 @@ function SidebarResizeHandle() {
729
733
  window.removeEventListener("pointercancel", onPointerUp);
730
734
  };
731
735
  }, [setWidth]);
732
- return (_jsx("div", { className: "absolute inset-y-0 right-0 z-20 hidden w-3 cursor-col-resize md:block", onPointerDown: (event) => {
736
+ return (_jsx("div", { "data-panel-resize": true, title: "Drag to resize navigation. Double-click to reset.", onDoubleClick: () => setWidth(SIDEBAR_WIDTH), className: "absolute inset-y-0 right-0 z-20 hidden w-3 cursor-col-resize md:block", onPointerDown: (event) => {
733
737
  event.preventDefault();
734
738
  dragState.current = {
735
739
  startX: event.clientX,
@@ -741,11 +745,11 @@ function SidebarResizeHandle() {
741
745
  }, onKeyDown: (event) => {
742
746
  if (event.key === "ArrowLeft") {
743
747
  event.preventDefault();
744
- setWidth(width - 16);
748
+ setWidth(width - (event.shiftKey ? 40 : 16));
745
749
  }
746
750
  if (event.key === "ArrowRight") {
747
751
  event.preventDefault();
748
- setWidth(width + 16);
752
+ setWidth(width + (event.shiftKey ? 40 : 16));
749
753
  }
750
754
  if (event.key === "Home") {
751
755
  event.preventDefault();
@@ -827,6 +831,17 @@ function useGroupCollapsed(label, defaultCollapsed) {
827
831
  // entry. ``map`` is parsed JSON so its prototype is ``Object``'s.
828
832
  return Object.prototype.hasOwnProperty.call(map, label) ? map[label] : Boolean(defaultCollapsed);
829
833
  }
834
+ /** Keep opened branches stable while routes and asynchronously loaded children change. */
835
+ function useTreeExpanded(to, inside) {
836
+ const key = `tree:${to}`;
837
+ const collapsed = useGroupCollapsed(key, !inside);
838
+ useEffect(() => {
839
+ if (inside && !Object.prototype.hasOwnProperty.call(readCollapseMap(), key)) {
840
+ writeCollapseMap({ ...readCollapseMap(), [key]: false });
841
+ }
842
+ }, [inside, key]);
843
+ return [!collapsed, (expanded) => writeCollapseMap({ ...readCollapseMap(), [key]: !expanded })];
844
+ }
830
845
  function toggleGroupCollapsed(label, current) {
831
846
  writeCollapseMap({ ...readCollapseMap(), [label]: !current });
832
847
  }
@@ -862,7 +877,7 @@ function NavGroupSection({ group }) {
862
877
  // that does not exist and reads as a bullet. The disclosure cell still
863
878
  // renders either way, so flat rows keep the same label column as tree
864
879
  // rows and the sidebar stays on one grid.
865
- const groupHasBranches = group.items.some((item) => Boolean(item.children && item.children.length > 0));
880
+ const groupHasBranches = group.items.some((item) => Boolean(!item.childrenHidden && item.children && item.children.length > 0));
866
881
  // hideLabel + collapsible are mutually exclusive — a hidden label
867
882
  // can't host a chevron. The collapsible flag is the opt-in; legacy
868
883
  // groups stay uncollapsable so behaviour is unchanged.
@@ -944,7 +959,7 @@ function NavGroupSection({ group }) {
944
959
  // that branches. Decided per GROUP: Workspaces has branches, so
945
960
  // its leaves are marked; Chats has none, so a dot on every row
946
961
  // would mark a distinction that does not exist.
947
- inTree: groupHasBranches, heading: focused, underHeader: !group.hideLabel }, item.to + item.label))) }, focusKey))] }));
962
+ inTree: group.tree ?? groupHasBranches, heading: focused, underHeader: !group.hideLabel }, item.to + item.label))) }, focusKey))] }));
948
963
  }
949
964
  /**
950
965
  * The "+" dropdown shown next to a group label when the group declares
@@ -960,9 +975,10 @@ function CreateActionsMenu({ group }) {
960
975
  action.handler();
961
976
  }, children: action.label }, action.id))) })] }));
962
977
  }
963
- function SidebarContent({ collapsed }) {
978
+ function SidebarContent({ collapsed, groups, onNewConversation, hideFooterActions, hideNewConversation, collapsedGroupsOnly, newConversationAfterGroup, newConversationLabel = "New Thread", }) {
964
979
  const toggle = useSidebarStore((s) => s.toggle);
965
- const handleNewConversation = useNewConversation();
980
+ const defaultNewConversation = useNewConversation();
981
+ const handleNewConversation = onNewConversation ?? defaultNewConversation;
966
982
  const rawNavConfig = useNavStore((s) => s.config);
967
983
  const navLoading = useNavStore((s) => s.navLoading);
968
984
  // Sort by `priority` descending so feature hooks can declare slot
@@ -970,15 +986,15 @@ function SidebarContent({ collapsed }) {
970
986
  // order in which addGroup() happened to be called. Stable sort: ties
971
987
  // preserve insertion order, so groups that don't set a priority keep
972
988
  // the legacy append-where-you-arrive behaviour.
973
- const navConfig = useMemo(() => [...rawNavConfig]
989
+ const navConfig = useMemo(() => [...(groups ?? rawNavConfig)]
974
990
  .map((g, i) => ({ g, i, p: g.priority ?? 0 }))
975
991
  .sort((a, b) => b.p - a.p || a.i - b.i)
976
- .map(({ g }) => g), [rawNavConfig]);
992
+ .map(({ g }) => g), [rawNavConfig, groups]);
977
993
  // Show skeleton rows during the cold nav fetch — but only until the
978
994
  // integrator's primary dynamic group ("Workspaces") has been published.
979
995
  // Once it exists, real rows render even if a later phase (status dots) is
980
996
  // still resolving, so the skeleton never flashes over a populated tree.
981
- const hasWorkspacesGroup = navConfig.some((group) => group.label === "Workspaces");
997
+ const hasWorkspacesGroup = navConfig.some((group) => group.label === "Workspaces" || group.meta?.type === "spaces-workspaces");
982
998
  const showNavSkeleton = navLoading && !hasWorkspacesGroup;
983
999
  const currentPage = useLocation().pathname;
984
1000
  const navContext = useAppStore((s) => s.navContext);
@@ -989,41 +1005,68 @@ function SidebarContent({ collapsed }) {
989
1005
  e.preventDefault();
990
1006
  }, []);
991
1007
  if (collapsed) {
992
- return (_jsxs("div", { className: "flex flex-col items-center gap-1 py-3 flex-1", children: [_jsx(TooltipIconButton, { tooltip: "Expand sidebar", side: "right", onClick: toggle, children: _jsx(PanelLeftOpen, { className: "size-5" }) }), _jsx(TooltipIconButton, { tooltip: "New Thread", side: "right", onClick: handleNewConversation, children: _jsx(SquarePen, { className: "size-4 text-sidebar-icon" }) }), _jsx("div", { className: "mt-4 flex w-full flex-col items-stretch gap-1", children: navConfig
993
- .map((group, groupIndex) => {
994
- // Modules with a non-flat collapsed rail (e.g. Spaces'
995
- // "header + active workspace") supply `collapsedRail`; the
996
- // shell stays module-agnostic.
997
- const items = group.collapsedRail
998
- ? group
999
- .collapsedRail({
1008
+ return (_jsxs("div", { className: "flex flex-col items-center gap-1 py-3 flex-1", children: [_jsx(TooltipIconButton, { tooltip: "Expand sidebar", side: "right", onClick: toggle, children: _jsx(PanelLeftOpen, { className: "size-5" }) }), !hideNewConversation && !collapsedGroupsOnly && (_jsx(TooltipIconButton, { tooltip: newConversationLabel, side: "right", onClick: handleNewConversation, children: _jsx(SquarePen, { className: "size-4 text-sidebar-icon" }) })), _jsx("div", { className: "mt-4 flex w-full flex-col items-stretch gap-1", children: collapsedGroupsOnly
1009
+ ? navConfig
1010
+ .filter((group) => !group.meta?.hideInCollapsedRail &&
1011
+ (group.to || group.items.length > 0) &&
1012
+ group.collapsedRail?.({ currentPage, navContextMeta: navContext.meta ?? {} })
1013
+ .length !== 0)
1014
+ .map((group) => {
1015
+ const rail = group.collapsedRail?.({
1000
1016
  currentPage,
1001
1017
  navContextMeta: navContext.meta ?? {},
1002
- })
1003
- .map((it) => ({
1004
- to: it.to,
1005
- label: it.label,
1006
- icon: it.icon,
1007
- isActive: it.isActive,
1008
- }))
1009
- : group.items.map((it) => ({
1010
- to: it.to,
1011
- label: it.label,
1012
- icon: it.icon,
1013
- isActive: currentPage === it.to,
1014
- }));
1015
- return { group, groupIndex, items };
1016
- })
1017
- .filter(({ items }) => items.length > 0)
1018
- .map(({ group, groupIndex, items }, visibleIndex) => (
1019
- // Each non-first VISIBLE group gets a thin top border —
1020
- // visually separates groups (e.g. user Workspaces vs
1021
- // Admin) in the collapsed rail without changing the
1022
- // CollapsedRailItem shape. The wrapper itself is a centred
1023
- // column so icons stay aligned with the rail.
1024
- _jsx("div", { className: cn("flex flex-col items-center gap-1", visibleIndex > 0 && "mt-2 pt-2 border-t border-sidebar-border/40"), children: items.map(({ to, label, icon, isActive }) => (_jsx(CollapsedNavShortcut, { to: to, label: label, icon: icon, isActive: isActive }, to))) }, `${group.label || ""}-${groupIndex}`))) })] }));
1018
+ });
1019
+ const active = currentPage === group.to ||
1020
+ group.items.some((item) => isInSubtree(currentPage, item)) ||
1021
+ Boolean(rail?.some((item) => item.isActive));
1022
+ const Icon = group.icon ??
1023
+ group.collapsedRail?.({ currentPage, navContextMeta: navContext.meta ?? {} })[0]
1024
+ ?.icon ??
1025
+ group.items[0]?.icon ??
1026
+ Layers;
1027
+ return group.to ? (_jsx(CollapsedNavShortcut, { to: group.to, label: group.label, icon: Icon, isActive: active }, group.label)) : (_jsx(TooltipIconButton, { tooltip: group.label, side: "right", "aria-label": `Open ${group.label}`, "aria-current": active ? "page" : undefined, className: active ? ACTIVE_NAV_ROW_CLASS : undefined, onClick: () => {
1028
+ toggleGroupCollapsed(group.label, true);
1029
+ toggle();
1030
+ }, children: _jsx(Icon, { className: "size-4" }) }, group.label));
1031
+ })
1032
+ : navConfig
1033
+ .map((group, groupIndex) => {
1034
+ // Modules with a non-flat collapsed rail (e.g. Spaces'
1035
+ // "header + active workspace") supply `collapsedRail`; the
1036
+ // shell stays module-agnostic.
1037
+ const items = group.collapsedRail
1038
+ ? group
1039
+ .collapsedRail({
1040
+ currentPage,
1041
+ navContextMeta: navContext.meta ?? {},
1042
+ })
1043
+ .map((it) => ({
1044
+ to: it.to,
1045
+ label: it.label,
1046
+ icon: it.icon,
1047
+ isActive: it.isActive,
1048
+ }))
1049
+ : group.items.map((it) => ({
1050
+ to: it.to,
1051
+ label: it.label,
1052
+ icon: it.icon,
1053
+ isActive: currentPage === it.to,
1054
+ }));
1055
+ return { group, groupIndex, items };
1056
+ })
1057
+ .filter(({ items }) => items.length > 0)
1058
+ .map(({ group, groupIndex, items }, visibleIndex) => (
1059
+ // Each non-first VISIBLE group gets a thin top border —
1060
+ // visually separates groups (e.g. user Workspaces vs
1061
+ // Admin) in the collapsed rail without changing the
1062
+ // CollapsedRailItem shape. The wrapper itself is a centred
1063
+ // column so icons stay aligned with the rail.
1064
+ _jsx("div", { className: cn("flex flex-col items-center gap-1", visibleIndex > 0 && "mt-2 pt-2 border-t border-sidebar-border/40"), children: items.map(({ to, label, icon, isActive }) => (_jsx(CollapsedNavShortcut, { to: to, label: label, icon: icon, isActive: isActive }, to))) }, `${group.label || ""}-${groupIndex}`))) })] }));
1025
1065
  }
1026
- return (_jsxs("div", { className: "flex flex-col flex-1 min-h-0", children: [_jsxs("div", { className: "flex items-center justify-between px-3 py-3", children: [_jsx(NavLink, { to: "/", className: "flex items-center gap-2 hover:opacity-80 transition-opacity", children: _jsx(AppBrand, { iconClassName: "text-primary", labelClassName: "truncate text-sm font-semibold text-sidebar-foreground" }) }), _jsx(TooltipIconButton, { tooltip: "Collapse sidebar", side: "right", onClick: toggle, children: _jsx(PanelLeft, { className: "size-5" }) })] }), _jsx("div", { className: "px-1 mb-0.5", children: _jsxs("button", { type: "button", onClick: handleNewConversation, className: cn("flex items-center gap-2 w-full", "rounded-xl px-3 py-2 text-sm text-left", "text-sidebar-foreground/85 transition-colors hover:bg-sidebar-accent/70"), children: [_jsx(SquarePen, { className: "size-4 shrink-0 text-sidebar-icon" }), "New Thread"] }) }), _jsxs("nav", { className: "flex-1 overflow-y-auto px-1 pb-16", onDragOverCapture: preventNativeFileDrop, onDropCapture: preventNativeFileDrop, children: [navConfig.map((group) => (_jsx(NavGroupSection, { group: group }, group.label))), showNavSkeleton && (_jsx("div", { className: "space-y-1 px-1 pt-2", "aria-hidden": "true", children: Array.from({ length: 6 }).map((_, index) => (_jsx("div", { className: "h-7 mx-1 rounded-lg animate-pulse bg-sidebar-accent/40" }, index))) }))] }), _jsx(SidebarFooterActions, {})] }));
1066
+ const newConversationAction = (_jsx("div", { className: "px-1 mb-0.5", children: _jsxs("button", { type: "button", onClick: handleNewConversation, className: cn("flex items-center gap-2 w-full", "rounded-xl px-3 py-2 text-sm text-left", "text-sidebar-foreground/85 transition-colors hover:bg-sidebar-accent/70"), children: [_jsx(SquarePen, { className: "size-4 shrink-0 text-sidebar-icon" }), newConversationLabel] }) }));
1067
+ return (_jsxs("div", { className: "flex flex-col flex-1 min-h-0", children: [_jsxs("div", { "data-navigation-header": true, className: "flex items-center justify-between px-3 py-3", children: [_jsx(NavLink, { to: "/", className: "flex items-center gap-2 hover:opacity-80 transition-opacity", children: _jsx(AppBrand, { iconClassName: "text-primary", labelClassName: "truncate text-sm font-semibold text-sidebar-foreground" }) }), _jsx(TooltipIconButton, { tooltip: "Collapse sidebar", side: "right", onClick: toggle, children: _jsx(PanelLeft, { className: "size-5" }) })] }), !hideNewConversation && !newConversationAfterGroup && newConversationAction, _jsxs("nav", { className: "flex-1 overflow-y-auto px-1 pb-16", onDragOverCapture: preventNativeFileDrop, onDropCapture: preventNativeFileDrop, children: [navConfig.map((group) => (_jsxs(Fragment, { children: [_jsx(NavGroupSection, { group: group }), !hideNewConversation &&
1068
+ group.label === newConversationAfterGroup &&
1069
+ newConversationAction] }, group.label))), showNavSkeleton && (_jsx("div", { className: "space-y-1 px-1 pt-2", "aria-hidden": "true", children: Array.from({ length: 6 }).map((_, index) => (_jsx("div", { className: "h-7 mx-1 rounded-lg animate-pulse bg-sidebar-accent/40" }, index))) }))] }), !hideFooterActions && _jsx(SidebarFooterActions, {})] }));
1027
1070
  }
1028
1071
  function SidebarFooterActions() {
1029
1072
  const footerActions = useNavStore((s) => s.footerActions);
@@ -1034,9 +1077,16 @@ function SidebarFooterActions() {
1034
1077
  : "border-sidebar-foreground/30") }), action.active ? (action.activeLabel ?? action.label) : action.label] }, action.id))) }));
1035
1078
  }
1036
1079
  /** Mobile drawer overlay */
1037
- function MobileDrawer() {
1080
+ function MobileDrawer({ footer, ...view }) {
1038
1081
  const isMobileOpen = useSidebarStore((s) => s.isMobileOpen);
1039
1082
  const closeMobile = useSidebarStore((s) => s.closeMobile);
1083
+ const locationKey = useLocation().key;
1084
+ const previousLocationKey = useRef(locationKey);
1085
+ useEffect(() => {
1086
+ if (previousLocationKey.current !== locationKey)
1087
+ closeMobile();
1088
+ previousLocationKey.current = locationKey;
1089
+ }, [locationKey, closeMobile]);
1040
1090
  const isToolPanelOpen = useToolPanelStore((s) => s.isOpen);
1041
1091
  const width = useSidebarStore((s) => s.width);
1042
1092
  const drawerWidth = Math.min(width, typeof window !== "undefined" ? window.innerWidth - 24 : width);
@@ -1052,14 +1102,14 @@ function MobileDrawer() {
1052
1102
  window.addEventListener("keydown", handleEscape);
1053
1103
  return () => window.removeEventListener("keydown", handleEscape);
1054
1104
  }, [isMobileOpen, isToolPanelOpen, closeMobile]);
1055
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: cn("fixed inset-0 z-50 bg-black/50 transition-opacity duration-200", isMobileOpen ? "opacity-100" : "opacity-0 pointer-events-none"), onClick: closeMobile, "aria-hidden": "true" }), _jsx("aside", { "data-sidebar-surface": "mobile", className: cn("fixed top-0 left-0 z-50 h-dvh", "flex flex-col", "border-r border-sidebar-border/60 bg-sidebar text-sidebar-foreground shadow-xl", "transition-transform duration-200 ease-in-out", isMobileOpen ? "translate-x-0" : "-translate-x-full"), style: { width: drawerWidth }, role: "complementary", "aria-label": "Navigation sidebar", children: _jsx(SidebarContent, { collapsed: false }) })] }));
1105
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: cn("fixed inset-0 z-50 bg-black/50 transition-opacity duration-200", isMobileOpen ? "opacity-100" : "opacity-0 pointer-events-none"), onClick: closeMobile, "aria-hidden": "true" }), _jsxs("aside", { "data-sidebar-surface": "mobile", className: cn("fixed top-0 left-0 z-50 h-dvh", "flex flex-col", "border-r border-sidebar-border/60 bg-sidebar text-sidebar-foreground shadow-xl", "transition-transform duration-200 ease-in-out", isMobileOpen ? "translate-x-0" : "-translate-x-full"), style: { width: drawerWidth }, role: "complementary", "aria-label": "Navigation sidebar", children: [_jsx(SidebarContent, { collapsed: false, ...view }), footer] })] }));
1056
1106
  }
1057
1107
  /** Desktop inline sidebar (expanded or collapsed rail) */
1058
- function DesktopSidebar() {
1108
+ function DesktopSidebar({ footer, collapsedFooter, ...view }) {
1059
1109
  const isOpen = useSidebarStore((s) => s.isOpen);
1060
1110
  const width = useSidebarStore((s) => s.width);
1061
- return (_jsxs("aside", { "data-sidebar-surface": "desktop", className: cn("group/sidebar relative hidden md:flex flex-col shrink-0", "h-full", "border-r border-sidebar-border/60 bg-sidebar text-sidebar-foreground", "transition-[width] duration-200 ease-in-out", "overflow-hidden"), style: { width: isOpen ? width : RAIL_WIDTH }, role: "complementary", "aria-label": "Navigation sidebar", children: [_jsx(SidebarContent, { collapsed: !isOpen }), isOpen && _jsx(SidebarResizeHandle, {})] }));
1111
+ return (_jsxs("aside", { "data-sidebar-surface": "desktop", "data-sidebar-collapsed": !isOpen || undefined, className: cn("group/sidebar relative hidden md:flex flex-col shrink-0", "h-full", "border-r border-sidebar-border/60 bg-sidebar text-sidebar-foreground", "transition-[width] duration-200 ease-in-out", "overflow-hidden"), style: { width: isOpen ? width : RAIL_WIDTH }, role: "complementary", "aria-label": "Navigation sidebar", children: [_jsx(SidebarContent, { collapsed: !isOpen, ...view }), isOpen ? footer : collapsedFooter, isOpen && _jsx(SidebarResizeHandle, {})] }));
1062
1112
  }
1063
- export function Sidebar() {
1064
- return (_jsxs(_Fragment, { children: [_jsx(MobileDrawer, {}), _jsx(DesktopSidebar, {})] }));
1113
+ export function Sidebar({ footer, collapsedFooter, ...view } = {}) {
1114
+ return (_jsxs(_Fragment, { children: [_jsx(MobileDrawer, { footer: footer, ...view }), _jsx(DesktopSidebar, { footer: footer, collapsedFooter: collapsedFooter, ...view })] }));
1065
1115
  }
@@ -5,6 +5,8 @@
5
5
  */
6
6
  interface ToolPanelLayoutProps {
7
7
  children: React.ReactNode;
8
+ /** Keep tools inside the working area, leaving sibling chat accessible. */
9
+ contained?: boolean;
8
10
  }
9
- export declare function ToolPanelLayout({ children }: ToolPanelLayoutProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function ToolPanelLayout({ children, contained }: ToolPanelLayoutProps): import("react/jsx-runtime").JSX.Element;
10
12
  export {};
@@ -2,11 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "@iloveagents/foundry-web-primitives";
3
3
  import { useToolPanelStore } from "../lib/tool-panel-store.js";
4
4
  import { ToolPanel } from "./tool-panel.js";
5
- export function ToolPanelLayout({ children }) {
6
- const isOpen = useToolPanelStore((state) => state.isOpen);
5
+ export function ToolPanelLayout({ children, contained = false }) {
6
+ const isOpen = useToolPanelStore((state) => state.isOpen && !state.isWindowHosted);
7
7
  const panelWidth = useToolPanelStore((state) => state.panelWidth);
8
8
  const useOverlay = useToolPanelStore((state) => state.useOverlay);
9
- return (_jsxs("div", { className: "relative h-full flex-1 min-w-0 overflow-hidden", children: [_jsx("div", { className: cn("h-full flex flex-col transition-all duration-300 ease-in-out", useOverlay && isOpen && "hidden"), style: {
10
- width: isOpen && !useOverlay ? `calc(100% - ${panelWidth}px)` : "100%",
11
- }, children: children }), _jsx(ToolPanel, {})] }));
9
+ return (_jsxs("div", { className: "relative h-full flex-1 min-w-0 overflow-hidden", children: [_jsx("div", { className: cn("h-full flex flex-col transition-all duration-300 ease-in-out", (contained || useOverlay) && isOpen && "hidden"), style: {
10
+ width: isOpen && !useOverlay && !contained ? `calc(100% - ${panelWidth}px)` : "100%",
11
+ }, children: children }), _jsx(ToolPanel, { contained: contained })] }));
12
12
  }
@@ -1 +1,8 @@
1
- export declare function ToolPanel(): import("react/jsx-runtime").JSX.Element | null;
1
+ import { type ToolPanelContent } from "../lib/tool-panel-store.js";
2
+ /** Renders panel content using the renderer registry, then built-in markdown/text. */
3
+ export declare function PanelContentRenderer({ content }: {
4
+ content: ToolPanelContent;
5
+ }): import("react/jsx-runtime").JSX.Element;
6
+ export declare function ToolPanel({ contained }: {
7
+ contained?: boolean;
8
+ }): import("react/jsx-runtime").JSX.Element | null;