@iloveagents/foundry-web-ui 0.29.1 → 0.31.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.
Files changed (48) hide show
  1. package/dist/components/ag-ui-runtime-provider.d.ts +8 -3
  2. package/dist/components/ag-ui-runtime-provider.js +76 -33
  3. package/dist/components/assistant-chat.d.ts +1 -15
  4. package/dist/components/assistant-chat.js +4 -5
  5. package/dist/components/chat-bubble.js +3 -4
  6. package/dist/components/chat-context-items.d.ts +6 -0
  7. package/dist/components/chat-context-items.js +25 -0
  8. package/dist/components/chat-context.js +10 -2
  9. package/dist/components/chat-empty-state.js +1 -1
  10. package/dist/components/chat-header.js +2 -1
  11. package/dist/components/composer-add-menu.d.ts +0 -19
  12. package/dist/components/composer-add-menu.js +2 -10
  13. package/dist/components/context-badges.d.ts +0 -14
  14. package/dist/components/context-badges.js +2 -29
  15. package/dist/components/context-bar.d.ts +1 -21
  16. package/dist/components/context-bar.js +3 -74
  17. package/dist/components/focus-chat-pane.js +1 -1
  18. package/dist/components/markdown-text.js +6 -4
  19. package/dist/components/sidebar.js +51 -17
  20. package/dist/components/tool-panel.js +1 -1
  21. package/dist/index.d.ts +6 -5
  22. package/dist/index.js +5 -4
  23. package/dist/lib/ag-ui-adapter.d.ts +3 -0
  24. package/dist/lib/ag-ui-adapter.js +75 -11
  25. package/dist/lib/auth-provider.js +1 -1
  26. package/dist/lib/chat-runs-store.d.ts +40 -0
  27. package/dist/lib/chat-runs-store.js +173 -0
  28. package/dist/lib/merge-chat-state.d.ts +3 -0
  29. package/dist/lib/merge-chat-state.js +21 -0
  30. package/dist/lib/nav-config.js +47 -20
  31. package/dist/lib/theme-runtime.d.ts +23 -0
  32. package/dist/lib/theme-runtime.js +210 -30
  33. package/dist/lib/theme-store.d.ts +3 -1
  34. package/dist/lib/theme-store.js +3 -2
  35. package/dist/lib/use-new-conversation.js +4 -8
  36. package/dist/styles.css +14 -0
  37. package/dist/workbench/chat-header.js +4 -3
  38. package/dist/workbench/conversation-history.d.ts +1 -1
  39. package/dist/workbench/conversation-history.js +75 -19
  40. package/dist/workbench/running-chats-menu.d.ts +3 -0
  41. package/dist/workbench/running-chats-menu.js +20 -0
  42. package/dist/workbench/selection-bridge.js +2 -5
  43. package/dist/workbench/use-workbench-state.d.ts +1 -2
  44. package/dist/workbench/use-workbench-state.js +15 -18
  45. package/dist/workbench/welcome-intro.js +1 -1
  46. package/dist/workbench/workbench-styles.js +54 -6
  47. package/dist/workbench/workbench.js +1 -1
  48. package/package.json +3 -3
@@ -22,15 +22,17 @@ export interface AGUIChatConversationFactoryArgs {
22
22
  export type AGUIHistoryAdapterFactory = (args: AGUIChatConversationFactoryArgs) => ThreadHistoryAdapter;
23
23
  interface AGUIRuntimeProviderProps {
24
24
  children: React.ReactNode;
25
+ headlessChildren?: React.ReactNode;
26
+ active?: boolean;
27
+ conversationUrl?: string;
25
28
  /** Custom fetch for agent requests. Handles URL rewriting + auth in production. */
26
29
  fetchFn?: typeof fetch;
27
30
  /**
28
31
  * Optional stable thread id for the AG-UI adapter. When set, the
29
32
  * agent's POST body carries this id so server-side persistence
30
33
  * (Foundry thread, conversation row) targets the same conversation
31
- * across reloads / resumes. Changing the id remounts the inner
32
- * runtime past messages from a different thread don't bleed
33
- * into the new one.
34
+ * across reloads / resumes. Changing the id selects an independent
35
+ * runtime; running conversations remain mounted in the background.
34
36
  */
35
37
  threadId?: string;
36
38
  /**
@@ -58,7 +60,10 @@ interface AGUIRuntimeProviderProps {
58
60
  interface AGUIContextValue {
59
61
  adapter: AGUIAdapterSDK;
60
62
  resetThread: () => void;
63
+ selectRetainedThread: (threadId: string) => boolean;
61
64
  }
62
65
  export declare function useAGUIAdapter(): AGUIContextValue;
66
+ /** Whether this runtime owns visible UI effects; standalone consumers remain active. */
67
+ export declare function useIsForegroundChat(): boolean;
63
68
  export declare function AGUIRuntimeProvider(props: AGUIRuntimeProviderProps): import("react/jsx-runtime").JSX.Element;
64
69
  export {};
@@ -1,5 +1,5 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, useContext, useState, useCallback, useMemo } from "react";
1
+ import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useState, useCallback, useMemo, useRef, useLayoutEffect, useEffect, } from "react";
3
3
  import { AssistantRuntimeProvider, useLocalRuntime } from "@assistant-ui/react";
4
4
  import { TooltipProvider } from "../ui/tooltip.js";
5
5
  import { AGUIAdapterSDK } from "../lib/ag-ui-adapter.js";
@@ -8,6 +8,7 @@ import { useAttachmentAdapterStore } from "../lib/attachment-registry.js";
8
8
  import { useVoiceAdapterStore } from "../lib/voice-adapter-registry.js";
9
9
  import { ShowDocumentToolUI } from "./show-document-tool-ui.js";
10
10
  import { ClientToolExecutor } from "./client-tool-executor.js";
11
+ import { useChatRuns, isChatRunActive, selectChatRun, warnBeforeChatUnload, disposeChatRuns, } from "../lib/chat-runs-store.js";
11
12
  const AGUIAdapterContext = createContext(null);
12
13
  export function useAGUIAdapter() {
13
14
  const context = useContext(AGUIAdapterContext);
@@ -16,37 +17,83 @@ export function useAGUIAdapter() {
16
17
  }
17
18
  return context;
18
19
  }
20
+ /** Whether this runtime owns visible UI effects; standalone consumers remain active. */
21
+ export function useIsForegroundChat() {
22
+ const context = useContext(AGUIAdapterContext);
23
+ return useChatRuns((state) => !context || state.foregroundId === null || state.foregroundId === context.adapter.threadId);
24
+ }
25
+ let mountedProviders = 0;
19
26
  export function AGUIRuntimeProvider(props) {
20
- // Splitting render across an outer wrapper + a keyed inner is the
21
- // canonical React idiom for "rebuild this stateful subtree when X
22
- // changes". assistant-ui's runtime is created inside useLocalRuntime
23
- // and tracks its messages internally; swapping threadId or history
24
- // mid-life would leave stale state. Keying the inner subtree on
25
- // threadId (+ the manual reset counter) guarantees a clean runtime
26
- // per conversation, matching the ChatGPT-style "click fresh chat
27
- // backed by past messages" UX.
28
- const [resetKey, setResetKey] = useState(0);
29
- const resetThread = useCallback(() => setResetKey((p) => p + 1), []);
30
- const innerKey = `${props.threadId ?? "fresh"}:${resetKey}`;
31
- return _jsx(AGUIRuntimeInner, { ...props, resetThread: resetThread }, innerKey);
27
+ const deletedThreads = useChatRuns((state) => state.deletedThreads);
28
+ const adapterCache = useRef(new Map()).current;
29
+ for (const threadId of Object.keys(deletedThreads))
30
+ adapterCache.delete(threadId);
31
+ const [freshId, setFreshId] = useState(() => crypto.randomUUID());
32
+ const id = props.threadId ?? freshId;
33
+ // Hosts with stable ids advance the id through the lifecycle callback.
34
+ const resetThread = useCallback(() => {
35
+ if (!props.threadId)
36
+ setFreshId(crypto.randomUUID());
37
+ }, [props.threadId]);
38
+ const selectRetainedThread = (threadId) => {
39
+ if (props.threadId)
40
+ return false;
41
+ setFreshId(threadId);
42
+ return true;
43
+ };
44
+ const [sessions, setSessions] = useState(() => [{ ...props, threadId: id }]);
45
+ if (!sessions.some((session) => session.threadId === id)) {
46
+ const runs = useChatRuns.getState().runs;
47
+ // Keep running runtimes mounted. Limit the cache of idle conversations.
48
+ const idle = sessions.filter((session) => !runs[session.threadId] || !isChatRunActive(runs[session.threadId]));
49
+ const retained = new Set(idle.slice(-19).map((session) => session.threadId));
50
+ setSessions([
51
+ ...sessions.filter((session) => retained.has(session.threadId) ||
52
+ (runs[session.threadId] && isChatRunActive(runs[session.threadId]))),
53
+ { ...props, threadId: id },
54
+ ]);
55
+ }
56
+ useLayoutEffect(() => {
57
+ if (props.active !== false)
58
+ selectChatRun(id);
59
+ }, [id, props.active]);
60
+ useEffect(() => {
61
+ mountedProviders++;
62
+ window.addEventListener("beforeunload", warnBeforeChatUnload);
63
+ return () => {
64
+ mountedProviders--;
65
+ queueMicrotask(() => {
66
+ if (mountedProviders === 0) {
67
+ window.removeEventListener("beforeunload", warnBeforeChatUnload);
68
+ disposeChatRuns();
69
+ }
70
+ });
71
+ };
72
+ }, []);
73
+ return (_jsx(_Fragment, { children: sessions
74
+ .filter((session) => !deletedThreads[session.threadId] || session.threadId === id)
75
+ .map((session) => (_jsxs(AGUIRuntimeInner, { ...session, deleted: Boolean(deletedThreads[session.threadId]), adapterCache: adapterCache, resetThread: resetThread, selectRetainedThread: selectRetainedThread, children: [session.headlessChildren, props.active !== false && session.threadId === id ? props.children : null] }, `${session.threadId}:${deletedThreads[session.threadId] ? "deleted" : "live"}`))) }));
32
76
  }
33
- function AGUIRuntimeInner({ children, fetchFn, threadId, urlMatch, historyAdapterFactory, resetThread, }) {
34
- const adapter = useMemo(() => new AGUIAdapterSDK("/api/agent",
35
- // Only pass options if at least one is set, mirroring the prior
36
- // single-arg semantics for the "fresh chat, no overrides" case.
37
- fetchFn || threadId ? { fetchFn, threadId } : undefined), [fetchFn, threadId]);
77
+ function AGUIRuntimeInner({ children, adapterCache, deleted, fetchFn, threadId, urlMatch, historyAdapterFactory, resetThread, selectRetainedThread, conversationUrl, }) {
78
+ const adapter = useMemo(() => {
79
+ // Evict expensive React runtimes, not the protocol session: hidden
80
+ // server messages and compaction state must survive a later revisit.
81
+ const retained = threadId ? adapterCache.get(threadId) : undefined;
82
+ if (retained)
83
+ return retained;
84
+ const created = new AGUIAdapterSDK("/api/agent", { fetchFn, threadId, conversationUrl });
85
+ if (!deleted)
86
+ adapterCache.set(created.threadId, created);
87
+ return created;
88
+ }, [adapterCache, fetchFn, threadId, conversationUrl, deleted]);
38
89
  // Hosts may register a custom adapter (see ``registerAttachmentAdapter``);
39
90
  // the shell's inline-base64 adapter is the default.
40
91
  const registeredAdapter = useAttachmentAdapterStore((s) => s.adapter);
41
92
  const attachmentAdapter = useMemo(() => registeredAdapter ?? new FileAttachmentAdapter(), [registeredAdapter]);
42
- // Build the history adapter ONCE per inner mount, after the AG-UI
43
- // adapter exists so we know its thread id. The keyed-remount in the
44
- // outer wrapper ensures threadId changes trigger a fresh adapter +
45
- // fresh history adapter together, so resumed chats hydrate from the
46
- // right conversation and fresh chats persist under their assistant-
47
- // ui-minted UUID.
48
- const historyAdapter = useMemo(() => {
49
- if (!historyAdapterFactory)
93
+ // Each retained runtime owns one history adapter, including after navigation.
94
+ // Changing the URL must never load persisted messages over an in-flight stream.
95
+ const [historyAdapter] = useState(() => {
96
+ if (deleted || !historyAdapterFactory)
50
97
  return undefined;
51
98
  return historyAdapterFactory({
52
99
  // ``urlMatch`` is the URL-derived resume signal (undefined for
@@ -57,11 +104,7 @@ function AGUIRuntimeInner({ children, fetchFn, threadId, urlMatch, historyAdapte
57
104
  urlMatch,
58
105
  aguiThreadId: adapter.threadId,
59
106
  });
60
- // adapter is stable for the life of this inner mount (keyed remount
61
- // covers the threadId change case), so it's OK to depend on the
62
- // factory + urlMatch here.
63
- // eslint-disable-next-line react-hooks/exhaustive-deps
64
- }, [historyAdapterFactory, urlMatch]);
107
+ });
65
108
  // Voice is opt-in and host-supplied (see ``registerVoiceAdapter``).
66
109
  // Both slots stay absent unless a module registered one, so a runtime
67
110
  // without voice is byte-for-byte the runtime that existed before:
@@ -81,5 +124,5 @@ function AGUIRuntimeInner({ children, fetchFn, threadId, urlMatch, historyAdapte
81
124
  ...(speechAdapter ? { speech: speechAdapter } : {}),
82
125
  },
83
126
  });
84
- return (_jsx(AGUIAdapterContext.Provider, { value: { adapter, resetThread }, children: _jsx(AssistantRuntimeProvider, { runtime: runtime, children: _jsxs(TooltipProvider, { children: [_jsx(ShowDocumentToolUI, {}), _jsx(ClientToolExecutor, {}), children] }) }) }));
127
+ return (_jsx(AGUIAdapterContext.Provider, { value: { adapter, resetThread, selectRetainedThread }, children: _jsx(AssistantRuntimeProvider, { runtime: runtime, children: _jsxs(TooltipProvider, { children: [children && _jsx(ShowDocumentToolUI, {}), children && _jsx(ClientToolExecutor, {}), children] }) }) }));
85
128
  }
@@ -11,9 +11,6 @@ export interface ChatContentProps {
11
11
  composerFooter?: ReactNode;
12
12
  /** Compact two-row composer with an auto-growing input. */
13
13
  compactComposer?: boolean;
14
- hiddenPageLabels?: string[];
15
- /** Hide the already-visible navigation page badge, without removing context. */
16
- hideCurrentPageBadge?: boolean;
17
14
  /** Optional empty conversation presentation; leaves the composer and runtime unchanged. */
18
15
  emptyState?: ReactNode;
19
16
  /**
@@ -23,17 +20,6 @@ export interface ChatContentProps {
23
20
  * workspace-aware list derived from app context.
24
21
  */
25
22
  starterSuggestions?: string[];
26
- /**
27
- * Show the page pin button in the composer.
28
- *
29
- * Defaults to TRUE. It defaulted to false, and nothing in the shell ever
30
- * passed it, so the affordance shipped switched off everywhere: on a
31
- * document or a view there was no way to put the thing you were looking
32
- * at into the conversation. Pinning the current page is the core gesture
33
- * of a page-aware chat, not an opt-in extra — an app that genuinely wants
34
- * a bare composer opts OUT.
35
- */
36
- showPinButton?: boolean;
37
23
  /**
38
24
  * Composer placeholder. Defaults to `DEFAULT_COMPOSER_PLACEHOLDER`.
39
25
  */
@@ -49,4 +35,4 @@ export declare const DEFAULT_COMPOSER_PLACEHOLDER = "Do anything";
49
35
  * Reusable chat content — used by ChatPage and ChatBubble.
50
36
  * Renders inside a ThreadPrimitive.Root context.
51
37
  */
52
- export declare function ChatContent({ centerComposer, composerIntro, composerFooter, compactComposer, hiddenPageLabels, hideCurrentPageBadge, emptyState, starterSuggestions, showPinButton, placeholder, }?: ChatContentProps): import("react/jsx-runtime").JSX.Element;
38
+ export declare function ChatContent({ centerComposer, composerIntro, composerFooter, compactComposer, emptyState, starterSuggestions, placeholder, }?: ChatContentProps): import("react/jsx-runtime").JSX.Element;
@@ -7,9 +7,8 @@ import { cn } from "@iloveagents/foundry-web-primitives";
7
7
  import { Button } from "@iloveagents/foundry-web-primitives";
8
8
  import { TooltipIconButton } from "./tooltip-icon-button.js";
9
9
  import { userAttachmentComponents, composerAttachmentComponents } from "./chat-attachments.js";
10
- import { SentContextBadges, ComposerContextBadges } from "./context-badges.js";
10
+ import { SentContextBadges } from "./context-badges.js";
11
11
  import { ComposerAddMenu } from "./composer-add-menu.js";
12
- import { ContextPins } from "./context-bar.js";
13
12
  import { ChatContext } from "./chat-context.js";
14
13
  import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
15
14
  import { ChatComposerActionsSlot, ChatComposerBannerSlot, ChatHomeStartersSlot, useChatThreadSurface, useComposerActionsOwnSend, } from "./chat-slots.js";
@@ -128,7 +127,7 @@ export const DEFAULT_COMPOSER_PLACEHOLDER = "Do anything";
128
127
  * Reusable chat content — used by ChatPage and ChatBubble.
129
128
  * Renders inside a ThreadPrimitive.Root context.
130
129
  */
131
- export function ChatContent({ centerComposer = false, composerIntro, composerFooter, compactComposer = false, hiddenPageLabels = [], hideCurrentPageBadge = false, emptyState, starterSuggestions = DEFAULT_STARTER_SUGGESTIONS, showPinButton = true, placeholder = DEFAULT_COMPOSER_PLACEHOLDER, } = {}) {
130
+ export function ChatContent({ centerComposer = false, composerIntro, composerFooter, compactComposer = false, emptyState, starterSuggestions = DEFAULT_STARTER_SUGGESTIONS, placeholder = DEFAULT_COMPOSER_PLACEHOLDER, } = {}) {
132
131
  const ThreadSurface = useChatThreadSurface();
133
132
  const isExpanded = useChatBubbleStore((s) => s.isExpanded);
134
133
  const collapseChat = useChatBubbleStore((s) => s.collapse);
@@ -138,7 +137,7 @@ export function ChatContent({ centerComposer = false, composerIntro, composerFoo
138
137
  // `overflow-hidden`, not scroll: a surface owns its own layout,
139
138
  // and a full-bleed one (an avatar cropped to the frame) must be
140
139
  // 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?" }), _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: {
140
+ _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-heading 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
141
  UserMessage,
143
142
  EditComposer,
144
143
  AssistantMessage,
@@ -146,7 +145,7 @@ export function ChatContent({ centerComposer = false, composerIntro, composerFoo
146
145
  // Query container for the controls inside, so they size
147
146
  // against the composer rather than the viewport. This one
148
147
  // is wide, so the effort picker keeps its full label.
149
- "@container", "flex flex-col gap-2", "rounded-3xl border border-input", "bg-background shadow-sm", "px-4 py-2", "focus-within:border-primary/50 focus-within:shadow-md", "transition-all duration-300"), children: [_jsx(ComposerSubmitBridge, {}), _jsx(ComposerContextBadges, { hideCurrentPage: hideCurrentPageBadge, hiddenPageLabels: hiddenPageLabels }), _jsx(ComposerPrimitive.Attachments, { components: composerAttachmentComponents }), _jsxs("div", { className: cn("flex items-center gap-2", compactComposer ? "flex-wrap" : "flex-wrap @md:flex-nowrap"), children: [_jsxs("div", { className: cn("flex shrink-0 items-center gap-1", compactComposer ? "order-2" : "order-2 @md:order-1"), children: [_jsx(ComposerAddMenu, { showPin: showPinButton }), _jsx(ContextPins, { allowPageToggle: showPinButton })] }), _jsx(ComposerInput, { autoFocus: true, placeholder: placeholder, rows: 1, className: cn("bg-transparent", compactComposer
148
+ "@container", "flex flex-col gap-2", "rounded-3xl border border-input", "bg-background shadow-sm", "px-4 py-2", "focus-within:border-primary/50 focus-within:shadow-md", "transition-all duration-300"), children: [_jsx(ComposerSubmitBridge, {}), _jsx(ComposerPrimitive.Attachments, { components: composerAttachmentComponents }), _jsxs("div", { className: cn("flex items-center gap-2", compactComposer ? "flex-wrap" : "flex-wrap @md:flex-nowrap"), children: [_jsx("div", { className: cn("flex shrink-0 items-center gap-1", compactComposer ? "order-2" : "order-2 @md:order-1"), children: _jsx(ComposerAddMenu, {}) }), _jsx(ComposerInput, { autoFocus: true, placeholder: placeholder, rows: 1, className: cn("bg-transparent", compactComposer
150
149
  ? "order-1 w-full"
151
150
  : "order-1 w-full @md:order-2 @md:w-auto @md:flex-1", "py-2 text-base sm:text-sm leading-relaxed", "text-foreground outline-none", "placeholder:text-muted-foreground", "resize-none max-h-32") }), _jsx("div", { className: cn("order-3 ml-auto flex shrink-0 items-center gap-2", !compactComposer && "@md:ml-0"), children: _jsx(ComposerActionButton, {}) })] })] }), composerFooter] })] })] })] }));
152
151
  }
@@ -7,8 +7,7 @@ import { Sparkles, X, Maximize2, SquarePen, ArrowUp, Square, ChevronDown, Copy,
7
7
  import { cn } from "@iloveagents/foundry-web-primitives";
8
8
  import { Button } from "@iloveagents/foundry-web-primitives";
9
9
  import { TooltipIconButton } from "./tooltip-icon-button.js";
10
- import { ContextPins } from "./context-bar.js";
11
- import { ComposerContextBadges, SentContextBadges } from "./context-badges.js";
10
+ import { SentContextBadges } from "./context-badges.js";
12
11
  import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
13
12
  import { ComposerAddMenu } from "./composer-add-menu.js";
14
13
  import { DEFAULT_COMPOSER_PLACEHOLDER } from "./assistant-chat.js";
@@ -109,9 +108,9 @@ export function ChatBubble() {
109
108
  : { 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
109
  UserMessage: BubbleUserMessage,
111
110
  AssistantMessage: BubbleAssistantMessage,
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(
111
+ } }), _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, {}), _jsx(ComposerPrimitive.Root, { className: cn(
113
112
  // Query container for the controls inside: the bubble is
114
113
  // a narrow panel in a wide window, so they have to size
115
114
  // against THIS box, not the viewport.
116
- "@container", "flex flex-col gap-1", "rounded-2xl border border-input", "bg-background shadow-sm", "px-3 py-2", "focus-within:border-primary/50", "transition-all duration-200"), children: [_jsx(ComposerContextBadges, {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [_jsx(ComposerAddMenu, { compact: true }), _jsx(ContextPins, { compact: true })] }), _jsx(ComposerInput, { autoFocus: true, placeholder: DEFAULT_COMPOSER_PLACEHOLDER, rows: 1, className: cn("flex-1 bg-transparent", "py-1 text-sm", "text-foreground outline-none", "placeholder:text-muted-foreground", "resize-none max-h-20") }), _jsx(ThreadPrimitive.If, { running: false, children: _jsx(ReasoningEffortPicker, {}) }), _jsx(ChatComposerActionsSlot, {}), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon", className: "rounded-full size-8 shrink-0", "aria-label": "Stop", children: _jsx(Square, { className: "size-3" }) }) }) }), !actionsOwnSend && (_jsx(ThreadPrimitive.If, { running: false, children: _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "icon", className: "rounded-full size-8 shrink-0", "aria-label": "Send", children: _jsx(ArrowUp, { className: "size-4" }) }) }) }))] })] })] })] })] })] }))] })] }));
115
+ "@container", "flex flex-col gap-1", "rounded-2xl border border-input", "bg-background shadow-sm", "px-3 py-2", "focus-within:border-primary/50", "transition-all duration-200"), children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex shrink-0 items-center gap-0.5", children: _jsx(ComposerAddMenu, { compact: true }) }), _jsx(ComposerInput, { autoFocus: true, placeholder: DEFAULT_COMPOSER_PLACEHOLDER, rows: 1, className: cn("flex-1 bg-transparent", "py-1 text-sm", "text-foreground outline-none", "placeholder:text-muted-foreground", "resize-none max-h-20") }), _jsx(ThreadPrimitive.If, { running: false, children: _jsx(ReasoningEffortPicker, {}) }), _jsx(ChatComposerActionsSlot, {}), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon", className: "rounded-full size-8 shrink-0", "aria-label": "Stop", children: _jsx(Square, { className: "size-3" }) }) }) }), !actionsOwnSend && (_jsx(ThreadPrimitive.If, { running: false, children: _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "icon", className: "rounded-full size-8 shrink-0", "aria-label": "Send", children: _jsx(ArrowUp, { className: "size-4" }) }) }) }))] }) })] })] })] })] }))] })] }));
117
116
  }
@@ -0,0 +1,6 @@
1
+ import { type ContextItem } from "../lib/app-store.js";
2
+ /** All added context is managed in the With menu, regardless of its lifetime. */
3
+ export declare function ChatContextItems({ items, onNavigate, }: {
4
+ items: ContextItem[];
5
+ onNavigate: () => void;
6
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { FileText, Link2, StickyNote, TextSelect, X } from "lucide-react";
3
+ import { useNavigate } from "react-router";
4
+ import { useAppStore } from "../lib/app-store.js";
5
+ import { getNavigablePath } from "./context-bar.js";
6
+ import { PinStateIcon } from "./pin-state-icon.js";
7
+ import { TooltipIconButton } from "./tooltip-icon-button.js";
8
+ const icons = { page: FileText, ref: Link2, note: StickyNote, selection: TextSelect };
9
+ /** All added context is managed in the With menu, regardless of its lifetime. */
10
+ export function ChatContextItems({ items, onNavigate, }) {
11
+ const navigate = useNavigate();
12
+ const remove = useAppStore((s) => s.removeContextItem);
13
+ const pin = useAppStore((s) => s.promoteContextItem);
14
+ if (!items.length)
15
+ return null;
16
+ return (_jsxs("section", { className: "border-t border-border pt-3", "aria-label": "Added context", children: [_jsx("h3", { className: "px-2 text-xs font-medium text-muted-foreground", children: "Added context" }), _jsx("ul", { className: "mt-1", children: items.map((item) => {
17
+ const Icon = icons[item.type];
18
+ const path = getNavigablePath(item);
19
+ const pinned = item.persistence === "persistent";
20
+ return (_jsxs("li", { className: "flex min-h-9 items-center gap-2 rounded-lg px-2 py-1", children: [_jsx(Icon, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1", children: [path ? (_jsx("button", { type: "button", className: "block w-full truncate text-left text-sm hover:underline", title: item.label, onClick: () => {
21
+ navigate(path);
22
+ onNavigate();
23
+ }, children: item.label })) : (_jsx("span", { className: "block truncate text-sm", title: item.label, children: item.label })), _jsx("span", { className: "block text-xs text-muted-foreground", children: pinned ? "Pinned" : item.key ? "Follows selection" : "Next message" })] }), !pinned && (_jsx(TooltipIconButton, { tooltip: "Keep in context", "aria-label": `Pin ${item.label}`, className: "size-8 shrink-0 text-muted-foreground", onClick: () => pin(item.id), children: _jsx(PinStateIcon, { pinned: false, className: "size-3.5" }) })), _jsx(TooltipIconButton, { tooltip: "Remove from context", "aria-label": `Remove ${item.label}`, className: "size-8 shrink-0 text-muted-foreground", onClick: () => remove(item.id), children: _jsx(X, { className: "size-3.5" }) })] }, item.id));
24
+ }) })] }));
25
+ }
@@ -5,6 +5,9 @@ import { useAppStore } from "../lib/app-store.js";
5
5
  import { summarizeChatContext } from "../lib/chat-context-summary.js";
6
6
  import { Popover, PopoverTrigger, PopoverContent } from "../ui/popover.js";
7
7
  import { ChatContextDetailsSlot } from "./chat-slots.js";
8
+ import { ChatContextItems } from "./chat-context-items.js";
9
+ import { usePagePin } from "./context-bar.js";
10
+ import { PinStateIcon } from "./pin-state-icon.js";
8
11
  function readContext() {
9
12
  return summarizeChatContext(useAppStore.getState().getAgentState());
10
13
  }
@@ -12,6 +15,11 @@ function readContext() {
12
15
  export function ChatContext() {
13
16
  const [open, setOpen] = useState(false);
14
17
  const [context, setContext] = useState(readContext);
18
+ const items = useAppStore((s) => s.contextItems);
19
+ const { isPinned, toggle } = usePagePin();
20
+ const selection = items.find((item) => item.key && item.persistence === "ephemeral");
21
+ const detail = context.detail || selection?.label || "";
22
+ const additionalCount = items.length - (selection && detail === selection.label ? 1 : 0);
15
23
  useEffect(() => {
16
24
  let frame = 0;
17
25
  const refresh = () => {
@@ -66,7 +74,7 @@ export function ChatContext() {
66
74
  };
67
75
  }, []);
68
76
  const Icon = context.field ? TextCursorInput : context.focused ? Maximize2 : FileText;
69
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-chat-context": true, "aria-label": `Message context: ${context.label}${context.detail ? `, ${context.detail}` : ""}`, className: "mb-2 flex min-h-9 w-full min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-primary [@media(pointer:coarse)]:min-h-11", children: [_jsx(Icon, { className: "size-3.5 shrink-0" }), _jsxs("span", { className: "min-w-0 flex-1 truncate", "aria-live": "polite", children: ["With ", _jsx("span", { className: "font-medium text-foreground", children: context.label }), context.detail && (_jsxs(_Fragment, { children: [" ", _jsx("span", { "aria-hidden": true, children: "\u203A" }), " ", _jsx("span", { className: "font-medium text-foreground", children: context.detail })] }))] }), _jsx(ChevronDown, { className: "size-3.5 shrink-0" })] }) }), _jsxs(PopoverContent, { align: "start", side: "top", collisionPadding: 12, "aria-label": "Context and actions", className: "w-[22rem] max-w-[calc(100vw-1.5rem)] max-h-[min(36rem,var(--radix-popover-content-available-height))] overflow-y-auto overscroll-contain space-y-3 p-3 text-sm", children: [_jsxs("div", { className: "px-2", children: [_jsx("h2", { className: "text-xs font-medium text-muted-foreground", children: "Current context" }), _jsxs("p", { className: "mt-1 break-words font-medium", children: [context.label, context.detail && ` › ${context.detail}`] })] }), _jsx("p", { className: "px-2 text-xs leading-relaxed text-muted-foreground", children: context.detail
77
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-chat-context": true, "aria-label": `Message context: ${context.label}${detail ? `, ${detail}` : ""}${additionalCount ? `, ${additionalCount} more` : ""}`, className: "mb-2 flex min-h-9 w-full min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-primary [@media(pointer:coarse)]:min-h-11", children: [_jsx(Icon, { className: "size-3.5 shrink-0" }), _jsxs("span", { className: "min-w-0 flex-1 truncate", "aria-live": "polite", children: ["With ", _jsx("span", { className: "font-medium text-foreground", children: context.label }), detail && (_jsxs(_Fragment, { children: [" ", _jsx("span", { "aria-hidden": true, children: "\u203A" }), " ", _jsx("span", { className: "font-medium text-foreground", children: detail })] }))] }), additionalCount > 0 && (_jsxs("span", { className: "shrink-0 tabular-nums", "aria-hidden": true, children: ["+", additionalCount] })), _jsx(ChevronDown, { className: "size-3.5 shrink-0" })] }) }), _jsxs(PopoverContent, { align: "start", side: "top", collisionPadding: 12, "aria-label": "Context and actions", className: "w-[22rem] max-w-[calc(100vw-1.5rem)] max-h-[min(36rem,var(--radix-popover-content-available-height))] overflow-y-auto overscroll-contain space-y-3 p-3 text-sm", children: [_jsxs("div", { className: "px-2", children: [_jsx("h2", { className: "text-xs font-medium text-muted-foreground", children: "Current context" }), _jsxs("p", { className: "mt-1 break-words font-medium", children: [context.label, detail && ` › ${detail}`] })] }), _jsx("p", { className: "px-2 text-xs leading-relaxed text-muted-foreground", children: detail
70
78
  ? "This selection stays available when you move into chat."
71
- : "The current page follows your navigation. Select text or a field to work more specifically." }), context.selectedText && (_jsx("blockquote", { className: "max-h-32 overflow-y-auto border-l-2 border-border pl-3 text-xs", children: context.selectedText })), _jsx(ChatContextDetailsSlot, { onDismiss: () => setOpen(false) }), context.references.length > 0 && (_jsxs("div", { className: "border-t border-border px-2 pt-3 text-xs", children: [_jsx("h3", { className: "font-medium", children: "Also open" }), _jsx("ul", { className: "mt-1 space-y-1 text-muted-foreground", children: context.references.map((label, index) => (_jsx("li", { className: "truncate", children: label }, `${label}-${index}`))) })] }))] })] }));
79
+ : "The current page follows your navigation. Select text or a field to work more specifically." }), context.selectedText && (_jsx("blockquote", { className: "max-h-32 overflow-y-auto border-l-2 border-border pl-3 text-xs", children: context.selectedText })), !context.focused && context.label !== "Your spaces" && (_jsxs("button", { type: "button", "aria-pressed": isPinned, onClick: toggle, className: "flex min-h-9 w-full items-center gap-2 rounded-lg px-2 text-left text-sm hover:bg-muted", children: [_jsx(PinStateIcon, { pinned: isPinned, className: "size-4 shrink-0 text-muted-foreground" }), isPinned ? "Unpin this page" : "Keep this page in context"] })), _jsx(ChatContextItems, { items: items, onNavigate: () => setOpen(false) }), _jsx(ChatContextDetailsSlot, { onDismiss: () => setOpen(false) }), context.references.length > 0 && (_jsxs("div", { className: "border-t border-border px-2 pt-3 text-xs", children: [_jsx("h3", { className: "font-medium", children: "Also open" }), _jsx("ul", { className: "mt-1 space-y-1 text-muted-foreground", children: context.references.map((label, index) => (_jsx("li", { className: "truncate", children: label }, `${label}-${index}`))) })] }))] })] }));
72
80
  }
@@ -2,5 +2,5 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { ChatHomeStartersSlot } from "./chat-slots.js";
3
3
  /** One compact conversation home for workbench, focus and embedded chat panes. */
4
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-4 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] }));
5
+ return (_jsxs("div", { "data-chat-empty-state": true, className: "flex min-h-full flex-col justify-start gap-3 px-4 pb-8 pt-3", children: [_jsx("h1", { className: "text-base leading-6 font-semibold", 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
6
  }
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { RunningChatsMenu } from "../workbench/running-chats-menu.js";
2
3
  import { Link } from "react-router";
3
4
  import { SquarePen, Menu } from "lucide-react";
4
5
  import { cn } from "@iloveagents/foundry-web-primitives";
@@ -19,5 +20,5 @@ export const ChatHeader = () => {
19
20
  const handleNewConversation = useNewConversation(isExpanded ? { navigateToChat: false, preserveNavigation: true } : undefined);
20
21
  const toggleMobile = useSidebarStore((s) => s.toggleMobile);
21
22
  const isSidebarOpen = useSidebarStore((s) => s.isOpen);
22
- return (_jsx("div", { className: cn("shrink-0", "bg-background border-b border-border"), children: _jsxs("div", { className: "px-4 py-3 flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(TooltipIconButton, { tooltip: "Open sidebar", onClick: toggleMobile, className: "md:hidden", children: _jsx(Menu, { className: "size-5" }) }), _jsx(Link, { to: "/", className: cn("flex items-center gap-2.5 hover:opacity-80 transition-opacity", isSidebarOpen && "md:hidden"), children: _jsx(AppBrand, { labelClassName: "text-sm font-semibold text-foreground" }) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ThemeToggle, {}), _jsx(TooltipIconButton, { tooltip: "New Thread", size: "icon", onClick: handleNewConversation, children: _jsx(SquarePen, { className: "size-4 text-primary" }) }), _jsx(UserMenu, {})] })] }) }));
23
+ return (_jsx("div", { className: cn("shrink-0", "bg-background border-b border-border"), children: _jsxs("div", { className: "px-4 py-3 flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(TooltipIconButton, { tooltip: "Open sidebar", onClick: toggleMobile, className: "md:hidden", children: _jsx(Menu, { className: "size-5" }) }), _jsx(Link, { to: "/", className: cn("flex items-center gap-2.5 hover:opacity-80 transition-opacity", isSidebarOpen && "md:hidden"), children: _jsx(AppBrand, { labelClassName: "text-sm font-semibold text-foreground" }) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(RunningChatsMenu, {}), _jsx(ThemeToggle, {}), _jsx(TooltipIconButton, { tooltip: "New Thread", size: "icon", onClick: handleNewConversation, children: _jsx(SquarePen, { className: "size-4 text-primary" }) }), _jsx(UserMenu, {})] })] }) }));
23
24
  };
@@ -1,25 +1,6 @@
1
- /**
2
- * The composer's leading "+" — one affordance for everything you can add to
3
- * a turn.
4
- *
5
- * It replaces a row of single-purpose icons (paperclip, pin, and whatever
6
- * came next). That row grows every time the product learns a new trick, and
7
- * each icon costs the user a guess about what it does; a labelled menu costs
8
- * one click and reads itself. Pinning in particular is a power feature that
9
- * does not deserve permanent residency next to the text field.
10
- *
11
- * Attaching still goes through `ComposerPrimitive.AddAttachment`, so the
12
- * file picker keeps whatever `accept` filter the registered attachment
13
- * adapter declares — reimplementing the picker here would drift from it.
14
- */
15
1
  import type { FC } from "react";
16
2
  export interface ComposerAddMenuProps {
17
3
  /** Bubble-composer sizing. */
18
4
  compact?: boolean;
19
- /**
20
- * Offer "Pin this page". A bare composer with no page behind it (or a host
21
- * that does not use page context) opts out.
22
- */
23
- showPin?: boolean;
24
5
  }
25
6
  export declare const ComposerAddMenu: FC<ComposerAddMenuProps>;
@@ -1,17 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { PinStateIcon } from "./pin-state-icon.js";
3
2
  import { ComposerPrimitive } from "@assistant-ui/react";
4
3
  import { Paperclip, Plus } from "lucide-react";
5
4
  import { Button, cn } from "@iloveagents/foundry-web-primitives";
6
5
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "../ui/dropdown-menu.js";
7
- import { usePagePin } from "./context-bar.js";
8
- export const ComposerAddMenu = ({ compact = false, showPin = true }) => {
9
- const { isPinned, toggle } = usePagePin();
6
+ export const ComposerAddMenu = ({ compact = false }) => {
10
7
  const icon = compact ? "size-3.5" : "size-4";
11
- return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": "Add to this message", className: cn("shrink-0 rounded-full", compact ? "size-7" : "size-9"), children: _jsx(Plus, { className: compact ? "size-4" : "size-5" }) }) }), _jsxs(DropdownMenuContent, { align: "start", side: "top", className: "w-52", children: [_jsx(ComposerPrimitive.AddAttachment, { asChild: true, children: _jsxs(DropdownMenuItem, { children: [_jsx(Paperclip, { className: icon }), "Attach file"] }) }), showPin && (_jsxs(DropdownMenuItem, { onSelect: (event) => {
12
- // Keep the menu anchored while the pin badge appears, so the
13
- // user sees the result of what they just clicked.
14
- event.preventDefault();
15
- toggle();
16
- }, children: [_jsx(PinStateIcon, { pinned: isPinned, className: icon }), isPinned ? "Unpin this page" : "Pin this page"] }))] })] }));
8
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": "Add to this message", className: cn("shrink-0 rounded-full", compact ? "size-7" : "size-9"), children: _jsx(Plus, { className: compact ? "size-4" : "size-5" }) }) }), _jsx(DropdownMenuContent, { align: "start", side: "top", className: "w-52", children: _jsx(ComposerPrimitive.AddAttachment, { asChild: true, children: _jsxs(DropdownMenuItem, { children: [_jsx(Paperclip, { className: icon }), "Attach file"] }) }) })] }));
17
9
  };
@@ -1,18 +1,4 @@
1
- /**
2
- * Context badge components for the composer and sent messages.
3
- *
4
- * Renders user-selected context items as violet-tinted badges,
5
- * visually distinct from gray file attachment badges.
6
- *
7
- * - ComposerContextBadges: editable badges in the composer (with remove button)
8
- * - SentContextBadges: read-only badges on sent user messages
9
- */
10
1
  import { type FC } from "react";
11
- /** Renders ephemeral context badges in the composer (persistent items shown in ContextPins). */
12
- export declare const ComposerContextBadges: FC<{
13
- hideCurrentPage?: boolean;
14
- hiddenPageLabels?: string[];
15
- }>;
16
2
  /** Renders sent context badges on a user message (read-only). */
17
3
  export declare const SentContextBadges: FC<{
18
4
  messageId: string;
@@ -1,16 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { PinStateIcon } from "./pin-state-icon.js";
3
- /**
4
- * Context badge components for the composer and sent messages.
5
- *
6
- * Renders user-selected context items as violet-tinted badges,
7
- * visually distinct from gray file attachment badges.
8
- *
9
- * - ComposerContextBadges: editable badges in the composer (with remove button)
10
- * - SentContextBadges: read-only badges on sent user messages
11
- */
12
2
  import { createElement } from "react";
13
- import { TextSelect, X, FileText, Link2, StickyNote } from "lucide-react";
3
+ import { TextSelect, FileText, Link2, StickyNote } from "lucide-react";
14
4
  import { cn } from "@iloveagents/foundry-web-primitives";
15
5
  import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip.js";
16
6
  import { useAppStore } from "../lib/app-store.js";
@@ -28,28 +18,11 @@ function getBadgeTooltip(item) {
28
18
  const source = item.type === "page" && item.payload.kind === "page" ? item.payload.path : item.sourcePage;
29
19
  return `${item.label}\n${source}`;
30
20
  }
31
- /** Editable badge in the composer with a remove button. */
32
- const ComposerBadge = ({ item }) => {
33
- const removeItem = useAppStore((s) => s.removeContextItem);
34
- const promoteItem = useAppStore((s) => s.promoteContextItem);
35
- const isEphemeral = item.persistence === "ephemeral";
36
- return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: cn("inline-flex items-center gap-1.5 max-w-70", "rounded-lg bg-primary/5", "px-2 py-1", isEphemeral ? "border border-dashed border-primary/40" : "border border-primary/20"), children: [_jsx(ContextIcon, { type: item.type, className: "size-3 text-primary shrink-0" }), _jsx("span", { className: "text-xs text-foreground truncate", children: item.label }), isEphemeral && (_jsx("button", { type: "button", onClick: () => promoteItem(item.id), className: "p-0.5 rounded hover:bg-primary/10 shrink-0", "aria-label": "Pin context", children: _jsx(PinStateIcon, { pinned: false, className: "size-3" }) })), _jsx("button", { type: "button", onClick: () => removeItem(item.id), className: "p-0.5 rounded hover:bg-primary/10 shrink-0", "aria-label": "Remove context", children: _jsx(X, { className: "size-3" }) })] }) }), _jsx(TooltipContent, { side: "top", className: "whitespace-pre-line", children: getBadgeTooltip(item) })] }));
37
- };
38
- /** Read-only badge on a sent user message. Mirrors composer context chips. */
21
+ /** Read-only badge on a sent user message. Records the context attached to that turn. */
39
22
  const SentBadge = ({ item }) => {
40
23
  const isEphemeral = item.persistence === "ephemeral";
41
24
  return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: cn("inline-flex items-center gap-1.5 max-w-70", "rounded-lg bg-primary/5", "px-2 py-1", isEphemeral ? "border border-dashed border-primary/40" : "border border-primary/20"), children: [_jsx(ContextIcon, { type: item.type, className: "size-3 text-primary shrink-0" }), _jsx("span", { className: "text-[11px] leading-tight text-foreground truncate", children: item.label })] }) }), _jsx(TooltipContent, { side: "top", className: "whitespace-pre-line", children: getBadgeTooltip(item) })] }));
42
25
  };
43
- /** Renders ephemeral context badges in the composer (persistent items shown in ContextPins). */
44
- export const ComposerContextBadges = ({ hideCurrentPage = false, hiddenPageLabels = [] }) => {
45
- const items = useAppStore((s) => s.contextItems);
46
- const ephemeral = items.filter((i) => i.persistence === "ephemeral" &&
47
- !(hideCurrentPage &&
48
- (i.payload.kind === "page" || (i.type === "note" && hiddenPageLabels.includes(i.label)))));
49
- if (ephemeral.length === 0)
50
- return null;
51
- return (_jsx("div", { className: "flex flex-wrap gap-1.5", children: ephemeral.map((item) => (_jsx(ComposerBadge, { item: item }, item.id))) }));
52
- };
53
26
  /** Renders sent context badges on a user message (read-only). */
54
27
  export const SentContextBadges = ({ messageId }) => {
55
28
  const items = useAppStore((s) => s.sentContext[messageId]);
@@ -1,20 +1,4 @@
1
- /**
2
- * Context Pins — pin button with count badge and hover flyout.
3
- *
4
- * Click: toggles pin/unpin current page.
5
- * Hover (when pins exist): shows flyout to view, navigate, and remove pins.
6
- */
7
- import { type FC } from "react";
8
1
  import { type ContextItem } from "../lib/app-store.js";
9
- interface ContextPinsProps {
10
- allowPageToggle?: boolean;
11
- /** Show the pin button to add current page */
12
- showPinButton?: boolean;
13
- /** Compact mode for small containers (mini chat bubble header) */
14
- compact?: boolean;
15
- /** Direction flyout opens: up (composer) or down (header) */
16
- flyoutDirection?: "up" | "down";
17
- }
18
2
  /** Resolve the URL a context item navigates to when clicked.
19
3
  *
20
4
  * Two shapes the framework knows about:
@@ -39,13 +23,9 @@ interface ContextPinsProps {
39
23
  */
40
24
  export declare function getNavigablePath(item: ContextItem): string | null;
41
25
  /**
42
- * The "is this page pinned, and how do I flip that" logic, shared by the
43
- * pin badge here and the composer's add-menu. Two call sites deciding
44
- * independently what "pinned" means is how they end up disagreeing.
26
+ * Pin the current navigation context from the With menu.
45
27
  */
46
28
  export declare function usePagePin(): {
47
29
  isPinned: boolean;
48
30
  toggle: () => void;
49
31
  };
50
- export declare const ContextPins: FC<ContextPinsProps>;
51
- export {};