@iloveagents/foundry-web-ui 0.29.0 → 0.30.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.
- package/dist/components/ag-ui-runtime-provider.d.ts +8 -3
- package/dist/components/ag-ui-runtime-provider.js +76 -33
- package/dist/components/assistant-chat.d.ts +1 -15
- package/dist/components/assistant-chat.js +3 -4
- package/dist/components/chat-bubble.js +3 -4
- package/dist/components/chat-context-items.d.ts +6 -0
- package/dist/components/chat-context-items.js +25 -0
- package/dist/components/chat-context.js +10 -2
- package/dist/components/chat-empty-state.js +1 -1
- package/dist/components/chat-header.js +2 -1
- package/dist/components/composer-add-menu.d.ts +0 -19
- package/dist/components/composer-add-menu.js +2 -10
- package/dist/components/context-badges.d.ts +0 -14
- package/dist/components/context-badges.js +2 -29
- package/dist/components/context-bar.d.ts +1 -21
- package/dist/components/context-bar.js +3 -74
- package/dist/components/focus-chat-pane.js +1 -1
- package/dist/components/sidebar.js +39 -10
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/lib/ag-ui-adapter.d.ts +3 -0
- package/dist/lib/ag-ui-adapter.js +75 -11
- package/dist/lib/chat-runs-store.d.ts +40 -0
- package/dist/lib/chat-runs-store.js +173 -0
- package/dist/lib/merge-chat-state.d.ts +3 -0
- package/dist/lib/merge-chat-state.js +21 -0
- package/dist/lib/nav-config.js +47 -20
- package/dist/lib/use-new-conversation.js +4 -8
- package/dist/workbench/chat-header.js +4 -3
- package/dist/workbench/conversation-history.d.ts +1 -1
- package/dist/workbench/conversation-history.js +75 -19
- package/dist/workbench/running-chats-menu.d.ts +3 -0
- package/dist/workbench/running-chats-menu.js +20 -0
- package/dist/workbench/use-workbench-state.d.ts +1 -2
- package/dist/workbench/use-workbench-state.js +15 -18
- package/dist/workbench/welcome-intro.js +1 -1
- package/dist/workbench/workbench-styles.js +63 -11
- package/dist/workbench/workbench.js +1 -1
- 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
|
|
32
|
-
* runtime
|
|
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 {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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(() =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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);
|
|
@@ -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(
|
|
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 {
|
|
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, {}),
|
|
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:
|
|
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}${
|
|
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-
|
|
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
|
-
|
|
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" }) }) }),
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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 {};
|