@iloveagents/foundry-web-ui 0.1.1 → 0.1.3
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/CHANGELOG.md +16 -0
- package/package.json +3 -3
- package/src/components/app-brand.tsx +1 -1
- package/src/components/assistant-chat.tsx +36 -18
- package/src/components/chat-bubble.tsx +14 -4
- package/src/components/theme-runtime-provider.tsx +1 -1
- package/src/index.ts +2 -1
- package/src/lib/__tests__/ag-ui-adapter.test.ts +54 -3
- package/src/lib/__tests__/app-store.test.ts +33 -0
- package/src/lib/ag-ui-adapter.ts +22 -14
- package/src/lib/app-store.ts +51 -2
- package/src/lib/theme-runtime.ts +2 -2
- package/src/lib/use-new-conversation.test.tsx +38 -0
- package/src/lib/use-new-conversation.ts +24 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @iloveagents/foundry-web-ui
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [d9b0460]
|
|
8
|
+
- @iloveagents/foundry-agent@0.1.3
|
|
9
|
+
- @iloveagents/foundry-web-primitives@0.1.3
|
|
10
|
+
|
|
11
|
+
## 0.1.2
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [30a4346]
|
|
16
|
+
- @iloveagents/foundry-agent@0.1.2
|
|
17
|
+
- @iloveagents/foundry-web-primitives@0.1.2
|
|
18
|
+
|
|
3
19
|
## 0.1.1
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"lucide-react": ">=0.400.0",
|
|
24
24
|
"@azure/msal-browser": "^5.0.0",
|
|
25
25
|
"@azure/msal-react": "^5.0.0",
|
|
26
|
-
"@iloveagents/foundry-agent": "0.1.
|
|
27
|
-
"@iloveagents/foundry-web-primitives": "0.1.
|
|
26
|
+
"@iloveagents/foundry-agent": "0.1.3",
|
|
27
|
+
"@iloveagents/foundry-web-primitives": "0.1.3"
|
|
28
28
|
},
|
|
29
29
|
"peerDependenciesMeta": {
|
|
30
30
|
"@azure/msal-browser": {
|
|
@@ -14,7 +14,7 @@ export function AppBrand({
|
|
|
14
14
|
labelClassName?: string;
|
|
15
15
|
}) {
|
|
16
16
|
const runtime = useThemeRuntime();
|
|
17
|
-
const brandName = runtime.branding.appName || "
|
|
17
|
+
const brandName = runtime.branding.appName || "eomi";
|
|
18
18
|
const logoUrl =
|
|
19
19
|
runtime.mode === "dark"
|
|
20
20
|
? runtime.branding.darkLogoUrl || runtime.branding.logoUrl
|
|
@@ -223,11 +223,15 @@ const ComposerActionButton: FC = () => {
|
|
|
223
223
|
);
|
|
224
224
|
};
|
|
225
225
|
|
|
226
|
-
/**
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Default starter prompts shown on empty state. Override per-app by passing
|
|
228
|
+
* a `starterSuggestions` prop to `<ChatContent>`. An empty array (`[]`)
|
|
229
|
+
* suppresses the suggestion buttons entirely.
|
|
230
|
+
*/
|
|
231
|
+
export const DEFAULT_STARTER_SUGGESTIONS = [
|
|
232
|
+
"Create a new Contract Space",
|
|
233
|
+
"List all contracts I have access to",
|
|
234
|
+
"Show me what I can do here",
|
|
231
235
|
];
|
|
232
236
|
|
|
233
237
|
const ScrollToBottomButton: FC = () => {
|
|
@@ -306,11 +310,23 @@ const DropZone: FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
|
306
310
|
);
|
|
307
311
|
};
|
|
308
312
|
|
|
313
|
+
export interface ChatContentProps {
|
|
314
|
+
/**
|
|
315
|
+
* Starter prompts shown when the thread is empty. Defaults to
|
|
316
|
+
* `DEFAULT_STARTER_SUGGESTIONS`. Pass an empty array to hide the
|
|
317
|
+
* suggestion buttons entirely. Customer apps (e.g. ANDRITZ Contracts)
|
|
318
|
+
* can pass a workspace-aware list derived from app context.
|
|
319
|
+
*/
|
|
320
|
+
starterSuggestions?: string[];
|
|
321
|
+
}
|
|
322
|
+
|
|
309
323
|
/**
|
|
310
324
|
* Reusable chat content — used by ChatPage and ChatBubble.
|
|
311
325
|
* Renders inside a ThreadPrimitive.Root context.
|
|
312
326
|
*/
|
|
313
|
-
export function ChatContent(
|
|
327
|
+
export function ChatContent({
|
|
328
|
+
starterSuggestions = DEFAULT_STARTER_SUGGESTIONS,
|
|
329
|
+
}: ChatContentProps = {}) {
|
|
314
330
|
const isExpanded = useChatBubbleStore((s) => s.isExpanded);
|
|
315
331
|
const collapseChat = useChatBubbleStore((s) => s.collapse);
|
|
316
332
|
const showPagePanel = useChatBubbleStore((s) => s.showPagePanel);
|
|
@@ -357,15 +373,17 @@ export function ChatContent() {
|
|
|
357
373
|
<h1 className="text-[2rem] font-normal text-foreground text-center mb-6">
|
|
358
374
|
How can I help you today?
|
|
359
375
|
</h1>
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
<
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
376
|
+
{starterSuggestions.length > 0 && (
|
|
377
|
+
<div className="flex flex-wrap justify-center gap-2 max-w-lg">
|
|
378
|
+
{starterSuggestions.map((prompt) => (
|
|
379
|
+
<ThreadPrimitive.Suggestion key={prompt} prompt={prompt} send asChild>
|
|
380
|
+
<Button variant="outline" className="text-sm">
|
|
381
|
+
{prompt}
|
|
382
|
+
</Button>
|
|
383
|
+
</ThreadPrimitive.Suggestion>
|
|
384
|
+
))}
|
|
385
|
+
</div>
|
|
386
|
+
)}
|
|
369
387
|
</div>
|
|
370
388
|
</ThreadPrimitive.Empty>
|
|
371
389
|
|
|
@@ -383,10 +401,10 @@ export function ChatContent() {
|
|
|
383
401
|
</ThreadPrimitive.If>
|
|
384
402
|
</div>
|
|
385
403
|
</ThreadPrimitive.If>
|
|
386
|
-
<ThreadPrimitive.If empty={false}>
|
|
387
|
-
<ScrollToBottomButton />
|
|
388
|
-
</ThreadPrimitive.If>
|
|
389
404
|
</ThreadPrimitive.Viewport>
|
|
405
|
+
<ThreadPrimitive.If empty={false}>
|
|
406
|
+
<ScrollToBottomButton />
|
|
407
|
+
</ThreadPrimitive.If>
|
|
390
408
|
</div>
|
|
391
409
|
|
|
392
410
|
<div className="shrink-0 bg-background">
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
MessageSquare,
|
|
12
12
|
X,
|
|
13
13
|
Maximize2,
|
|
14
|
+
SquarePen,
|
|
14
15
|
ArrowUp,
|
|
15
16
|
Square,
|
|
16
17
|
ChevronDown,
|
|
@@ -28,6 +29,7 @@ import { MarkdownText } from "./markdown-text.tsx";
|
|
|
28
29
|
import { LoadingIndicator } from "./loading-indicator.tsx";
|
|
29
30
|
import { useAppStore } from "../lib/app-store.ts";
|
|
30
31
|
import { useChatBubbleStore } from "../lib/chat-bubble-store.ts";
|
|
32
|
+
import { useNewConversation } from "../lib/use-new-conversation.ts";
|
|
31
33
|
import { RAIL_WIDTH, useSidebarStore } from "../lib/sidebar-store.ts";
|
|
32
34
|
|
|
33
35
|
const CHAT_BUBBLE_INSET = 8;
|
|
@@ -106,6 +108,7 @@ export function ChatBubble() {
|
|
|
106
108
|
const openBubble = useChatBubbleStore((s) => s.open);
|
|
107
109
|
const closeBubble = useChatBubbleStore((s) => s.close);
|
|
108
110
|
const expandChat = useChatBubbleStore((s) => s.expand);
|
|
111
|
+
const startNewThread = useNewConversation({ navigateToChat: false, preserveNavigation: true });
|
|
109
112
|
const isSidebarOpen = useSidebarStore((s) => s.isOpen);
|
|
110
113
|
const sidebarWidth = useSidebarStore((s) => s.width);
|
|
111
114
|
const pageLabel = useAppStore((s) => s.navContext.label);
|
|
@@ -239,6 +242,14 @@ export function ChatBubble() {
|
|
|
239
242
|
</span>
|
|
240
243
|
</div>
|
|
241
244
|
<div className="flex items-center gap-0.5">
|
|
245
|
+
<TooltipIconButton
|
|
246
|
+
tooltip="New Thread"
|
|
247
|
+
size="icon"
|
|
248
|
+
className="size-7"
|
|
249
|
+
onClick={startNewThread}
|
|
250
|
+
>
|
|
251
|
+
<SquarePen className="size-3.5" />
|
|
252
|
+
</TooltipIconButton>
|
|
242
253
|
<TooltipIconButton
|
|
243
254
|
tooltip="Expand chat"
|
|
244
255
|
size="icon"
|
|
@@ -283,11 +294,10 @@ export function ChatBubble() {
|
|
|
283
294
|
</ThreadPrimitive.If>
|
|
284
295
|
</div>
|
|
285
296
|
</ThreadPrimitive.If>
|
|
286
|
-
|
|
287
|
-
<ThreadPrimitive.If empty={false}>
|
|
288
|
-
<BubbleScrollToBottomButton />
|
|
289
|
-
</ThreadPrimitive.If>
|
|
290
297
|
</ThreadPrimitive.Viewport>
|
|
298
|
+
<ThreadPrimitive.If empty={false}>
|
|
299
|
+
<BubbleScrollToBottomButton />
|
|
300
|
+
</ThreadPrimitive.If>
|
|
291
301
|
</div>
|
|
292
302
|
|
|
293
303
|
{/* Composer — same pattern as full chat: clip + pin + input + send */}
|
|
@@ -86,7 +86,7 @@ export function ThemeDocumentMetadata() {
|
|
|
86
86
|
|
|
87
87
|
useEffect(() => {
|
|
88
88
|
if (typeof document === "undefined") return;
|
|
89
|
-
document.title = runtime.branding.appTitle || runtime.branding.appName || "
|
|
89
|
+
document.title = runtime.branding.appTitle || runtime.branding.appName || "eomi";
|
|
90
90
|
}, [runtime.branding.appName, runtime.branding.appTitle]);
|
|
91
91
|
|
|
92
92
|
return null;
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,8 @@ export { ToolPanelLayout } from "./components/tool-panel-layout.tsx";
|
|
|
5
5
|
export { MarkdownText, markdownComponents } from "./components/markdown-text.tsx";
|
|
6
6
|
export { ClientToolExecutor } from "./components/client-tool-executor.tsx";
|
|
7
7
|
export { AGUIRuntimeProvider, useAGUIAdapter } from "./components/ag-ui-runtime-provider.tsx";
|
|
8
|
-
export { ChatContent } from "./components/assistant-chat.tsx";
|
|
8
|
+
export { ChatContent, DEFAULT_STARTER_SUGGESTIONS } from "./components/assistant-chat.tsx";
|
|
9
|
+
export type { ChatContentProps } from "./components/assistant-chat.tsx";
|
|
9
10
|
export { ChatBubble } from "./components/chat-bubble.tsx";
|
|
10
11
|
export { ChatHeader } from "./components/chat-header.tsx";
|
|
11
12
|
export {
|
|
@@ -16,7 +16,10 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
16
16
|
import type { RunnerEvent } from "@iloveagents/foundry-agent";
|
|
17
17
|
|
|
18
18
|
const appStoreMock = vi.hoisted(() => ({
|
|
19
|
-
agentState: {
|
|
19
|
+
agentState: {
|
|
20
|
+
contextItems: [] as Array<Record<string, unknown>>,
|
|
21
|
+
navigation: { contextInstructions: null as string | null },
|
|
22
|
+
},
|
|
20
23
|
consumedMessageIds: [] as string[],
|
|
21
24
|
}));
|
|
22
25
|
|
|
@@ -73,7 +76,9 @@ import { AGUIAdapterSDK } from "../ag-ui-adapter.ts";
|
|
|
73
76
|
|
|
74
77
|
function makeRunInput() {
|
|
75
78
|
return {
|
|
76
|
-
messages: [
|
|
79
|
+
messages: [
|
|
80
|
+
{ id: "u-1", role: "user" as const, content: [{ type: "text" as const, text: "hi" }] },
|
|
81
|
+
],
|
|
77
82
|
abortSignal: undefined as AbortSignal | undefined,
|
|
78
83
|
};
|
|
79
84
|
}
|
|
@@ -82,7 +87,10 @@ describe("AGUIAdapterSDK", () => {
|
|
|
82
87
|
beforeEach(() => {
|
|
83
88
|
runMock.mockReset();
|
|
84
89
|
runnerMock.mockClear();
|
|
85
|
-
appStoreMock.agentState = {
|
|
90
|
+
appStoreMock.agentState = {
|
|
91
|
+
contextItems: [],
|
|
92
|
+
navigation: { contextInstructions: null },
|
|
93
|
+
};
|
|
86
94
|
appStoreMock.consumedMessageIds = [];
|
|
87
95
|
});
|
|
88
96
|
|
|
@@ -186,6 +194,7 @@ describe("AGUIAdapterSDK", () => {
|
|
|
186
194
|
|
|
187
195
|
it("forwards app context items through the native AG-UI context field", async () => {
|
|
188
196
|
appStoreMock.agentState = {
|
|
197
|
+
navigation: { contextInstructions: null },
|
|
189
198
|
contextItems: [
|
|
190
199
|
{
|
|
191
200
|
type: "ref",
|
|
@@ -214,6 +223,48 @@ describe("AGUIAdapterSDK", () => {
|
|
|
214
223
|
]);
|
|
215
224
|
expect(appStoreMock.consumedMessageIds).toEqual(["u-1"]);
|
|
216
225
|
});
|
|
226
|
+
|
|
227
|
+
it("describes typed JSON ref targets without losing the stable ref handle", async () => {
|
|
228
|
+
appStoreMock.agentState = {
|
|
229
|
+
navigation: { contextInstructions: null },
|
|
230
|
+
contextItems: [
|
|
231
|
+
{
|
|
232
|
+
type: "ref",
|
|
233
|
+
label: "Page Instructions · Test Page",
|
|
234
|
+
refType: "entity-field",
|
|
235
|
+
refId: "entity-1:instructions.agent_instructions",
|
|
236
|
+
preview: "Current: old\nProposed: new",
|
|
237
|
+
targetPath: "$.instructions.agent_instructions",
|
|
238
|
+
target: {
|
|
239
|
+
kind: "json-field",
|
|
240
|
+
path: "$.instructions.agent_instructions",
|
|
241
|
+
label: "Page Instructions",
|
|
242
|
+
scope: "entity-field",
|
|
243
|
+
containerId: "entity-1",
|
|
244
|
+
containerLabel: "Test Page",
|
|
245
|
+
currentValue: "old",
|
|
246
|
+
proposedValue: "new",
|
|
247
|
+
changeIds: ["change-1"],
|
|
248
|
+
},
|
|
249
|
+
sourcePage: "/spaces/entity-1",
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
};
|
|
253
|
+
runMock.mockImplementation(async function* (): AsyncGenerator<RunnerEvent> {});
|
|
254
|
+
|
|
255
|
+
const adapter = new AGUIAdapterSDK();
|
|
256
|
+
for await (const _ of adapter.run(makeRunInput())) {
|
|
257
|
+
// Drain the adapter.
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
expect(runMock.mock.calls[0][0].context).toEqual([
|
|
261
|
+
{
|
|
262
|
+
description:
|
|
263
|
+
"Selected entity-field Page Instructions. Use targetPath as the exact JSON edit target when the user asks to modify, rewrite, or transform this reference.",
|
|
264
|
+
value: JSON.stringify(appStoreMock.agentState.contextItems[0]),
|
|
265
|
+
},
|
|
266
|
+
]);
|
|
267
|
+
});
|
|
217
268
|
});
|
|
218
269
|
|
|
219
270
|
interface ChatModelRunResultLike {
|
|
@@ -339,6 +339,39 @@ describe("app-store", () => {
|
|
|
339
339
|
expect(projection.navigation.groupMeta).toEqual({ orgId: "org-1" });
|
|
340
340
|
expect(projection.navigation.group).toBe("Contracts");
|
|
341
341
|
});
|
|
342
|
+
|
|
343
|
+
it("compacts reference target values within the documented context limit", () => {
|
|
344
|
+
const longValue = "x".repeat(2_001);
|
|
345
|
+
|
|
346
|
+
store().addContextItem({
|
|
347
|
+
type: "ref",
|
|
348
|
+
label: "Long field",
|
|
349
|
+
sourcePage: "/spaces/entity-1",
|
|
350
|
+
persistence: "ephemeral",
|
|
351
|
+
payload: {
|
|
352
|
+
kind: "ref",
|
|
353
|
+
refType: "entity-field",
|
|
354
|
+
refId: "entity-1",
|
|
355
|
+
target: {
|
|
356
|
+
path: "$.instructions.page",
|
|
357
|
+
label: "Page Instructions",
|
|
358
|
+
currentValue: longValue,
|
|
359
|
+
proposedValue: longValue,
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const projection = store().getAgentState();
|
|
365
|
+
const target = projection.contextItems[0].target as {
|
|
366
|
+
currentValue: string;
|
|
367
|
+
proposedValue: string;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
expect(target.currentValue).toHaveLength(2_000);
|
|
371
|
+
expect(target.currentValue.endsWith("...")).toBe(true);
|
|
372
|
+
expect(target.proposedValue).toHaveLength(2_000);
|
|
373
|
+
expect(target.proposedValue.endsWith("...")).toBe(true);
|
|
374
|
+
});
|
|
342
375
|
});
|
|
343
376
|
|
|
344
377
|
describe("resetAll", () => {
|
package/src/lib/ag-ui-adapter.ts
CHANGED
|
@@ -25,11 +25,7 @@ import type {
|
|
|
25
25
|
ToolCallMessagePart,
|
|
26
26
|
} from "@assistant-ui/react";
|
|
27
27
|
import type { Context, Message as AGUIMessage } from "@ag-ui/core";
|
|
28
|
-
import {
|
|
29
|
-
AGUIRunner,
|
|
30
|
-
clientToolRegistry,
|
|
31
|
-
streamingStatusStore,
|
|
32
|
-
} from "@iloveagents/foundry-agent";
|
|
28
|
+
import { AGUIRunner, clientToolRegistry, streamingStatusStore } from "@iloveagents/foundry-agent";
|
|
33
29
|
import { tokenFetch } from "@iloveagents/foundry-agent/msal";
|
|
34
30
|
import { useAppStore, type AgentStateProjection } from "./app-store.ts";
|
|
35
31
|
import { useDevStore } from "./dev-store.ts";
|
|
@@ -45,10 +41,7 @@ interface ToolCallSnapshot {
|
|
|
45
41
|
export class AGUIAdapterSDK implements ChatModelAdapter {
|
|
46
42
|
private readonly runner: AGUIRunner;
|
|
47
43
|
|
|
48
|
-
constructor(
|
|
49
|
-
url: string = "/api/agent",
|
|
50
|
-
options?: { threadId?: string; fetchFn?: typeof fetch },
|
|
51
|
-
) {
|
|
44
|
+
constructor(url: string = "/api/agent", options?: { threadId?: string; fetchFn?: typeof fetch }) {
|
|
52
45
|
this.runner = new AGUIRunner({
|
|
53
46
|
url,
|
|
54
47
|
threadId: options?.threadId,
|
|
@@ -91,9 +84,7 @@ export class AGUIAdapterSDK implements ChatModelAdapter {
|
|
|
91
84
|
* generator returns. assistant-ui leaves the message stuck in the
|
|
92
85
|
* loading state if the last yield's status is `running`.
|
|
93
86
|
*/
|
|
94
|
-
const buildResult = (
|
|
95
|
-
statusOverride?: ChatModelRunResult["status"],
|
|
96
|
-
): ChatModelRunResult => {
|
|
87
|
+
const buildResult = (statusOverride?: ChatModelRunResult["status"]): ChatModelRunResult => {
|
|
97
88
|
const content: (ToolCallMessagePart | TextMessagePart)[] = [];
|
|
98
89
|
|
|
99
90
|
for (const [id, tc] of toolCalls) {
|
|
@@ -278,8 +269,21 @@ function describeContextItem(item: Record<string, unknown>, index: number): stri
|
|
|
278
269
|
|
|
279
270
|
if (type === "ref") {
|
|
280
271
|
const refType = typeof item.refType === "string" ? item.refType : "reference";
|
|
281
|
-
|
|
282
|
-
|
|
272
|
+
const target = isRecord(item.target) ? item.target : undefined;
|
|
273
|
+
const targetPath =
|
|
274
|
+
typeof item.targetPath === "string"
|
|
275
|
+
? item.targetPath
|
|
276
|
+
: typeof target?.path === "string"
|
|
277
|
+
? target.path
|
|
278
|
+
: undefined;
|
|
279
|
+
const targetLabel = typeof target?.label === "string" ? target.label : undefined;
|
|
280
|
+
const targetScope = typeof target?.scope === "string" ? target.scope : undefined;
|
|
281
|
+
if (targetPath) {
|
|
282
|
+
if (!targetLabel && !targetScope) {
|
|
283
|
+
return `Selected ${refType} atom. Use targetPath as the exact edit target when the user asks to modify, rewrite, or transform selected content.`;
|
|
284
|
+
}
|
|
285
|
+
const targetText = targetLabel || targetScope || "JSON atom";
|
|
286
|
+
return `Selected ${refType} ${targetText}. Use targetPath as the exact JSON edit target when the user asks to modify, rewrite, or transform this reference.`;
|
|
283
287
|
}
|
|
284
288
|
return `Selected ${refType} reference. Use refId as the edit target when the user asks to modify, rewrite, or transform selected content.`;
|
|
285
289
|
}
|
|
@@ -305,6 +309,10 @@ function stringifyContextItem(item: Record<string, unknown>): string {
|
|
|
305
309
|
}
|
|
306
310
|
}
|
|
307
311
|
|
|
312
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
313
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
314
|
+
}
|
|
315
|
+
|
|
308
316
|
/**
|
|
309
317
|
* Convert assistant-ui's `Message[]` into AG-UI's wire format.
|
|
310
318
|
*
|
package/src/lib/app-store.ts
CHANGED
|
@@ -33,6 +33,25 @@ export interface ReferencePayload {
|
|
|
33
33
|
refId: string;
|
|
34
34
|
/** Optional display text (~100 chars) */
|
|
35
35
|
preview?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Optional exact JSON target inside the referenced domain object.
|
|
38
|
+
*
|
|
39
|
+
* This keeps the existing refType/refId contract stable for deep links
|
|
40
|
+
* while allowing feature modules to target one schema-declared field,
|
|
41
|
+
* table cell, JSON atom, or app-defined custom value precisely.
|
|
42
|
+
*/
|
|
43
|
+
target?: {
|
|
44
|
+
kind?: string;
|
|
45
|
+
path?: string;
|
|
46
|
+
label?: string;
|
|
47
|
+
scope?: string;
|
|
48
|
+
containerId?: string;
|
|
49
|
+
containerLabel?: string;
|
|
50
|
+
currentValue?: unknown;
|
|
51
|
+
proposedValue?: unknown;
|
|
52
|
+
changeIds?: string[];
|
|
53
|
+
meta?: Record<string, unknown>;
|
|
54
|
+
};
|
|
36
55
|
/** Domain-specific data the agent can use */
|
|
37
56
|
meta?: Record<string, unknown>;
|
|
38
57
|
}
|
|
@@ -82,6 +101,8 @@ export interface AgentStateProjection {
|
|
|
82
101
|
|
|
83
102
|
const MAX_ITEMS = 10;
|
|
84
103
|
const MAX_LABEL_LENGTH = 500;
|
|
104
|
+
const MAX_CONTEXT_VALUE_LENGTH = 2_000;
|
|
105
|
+
const TRUNCATION_SUFFIX = "...";
|
|
85
106
|
|
|
86
107
|
const DEFAULT_NAV_CONTEXT: NavContext = {
|
|
87
108
|
group: null,
|
|
@@ -97,7 +118,11 @@ function refDedupKey(payload: ReferencePayload): string {
|
|
|
97
118
|
const selectionScope =
|
|
98
119
|
typeof payload.meta?.selectionScope === "string" ? payload.meta.selectionScope : "block";
|
|
99
120
|
const targetPath =
|
|
100
|
-
typeof payload.
|
|
121
|
+
typeof payload.target?.path === "string"
|
|
122
|
+
? payload.target.path
|
|
123
|
+
: typeof payload.meta?.targetPath === "string"
|
|
124
|
+
? payload.meta.targetPath
|
|
125
|
+
: undefined;
|
|
101
126
|
|
|
102
127
|
if (selectionScope === "atom" && targetPath) {
|
|
103
128
|
return `${payload.refType}:${payload.refId}:atom:${targetPath}`;
|
|
@@ -108,6 +133,24 @@ function refDedupKey(payload: ReferencePayload): string {
|
|
|
108
133
|
return `${payload.refType}:${payload.refId}:block`;
|
|
109
134
|
}
|
|
110
135
|
|
|
136
|
+
function compactContextValue(value: unknown): unknown {
|
|
137
|
+
if (typeof value === "string" && value.length > MAX_CONTEXT_VALUE_LENGTH) {
|
|
138
|
+
return `${value.slice(0, MAX_CONTEXT_VALUE_LENGTH - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}`;
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function compactReferenceTarget(
|
|
144
|
+
target: ReferencePayload["target"],
|
|
145
|
+
): ReferencePayload["target"] {
|
|
146
|
+
if (!target) return undefined;
|
|
147
|
+
return {
|
|
148
|
+
...target,
|
|
149
|
+
currentValue: compactContextValue(target.currentValue),
|
|
150
|
+
proposedValue: compactContextValue(target.proposedValue),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
111
154
|
interface AppState {
|
|
112
155
|
// Thread state
|
|
113
156
|
threadActive: boolean;
|
|
@@ -282,12 +325,18 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
282
325
|
case "ref":
|
|
283
326
|
return {
|
|
284
327
|
type,
|
|
328
|
+
label: label.slice(0, 200),
|
|
285
329
|
refType: payload.refType,
|
|
286
330
|
refId: payload.refId,
|
|
287
331
|
preview: payload.preview?.slice(0, 100),
|
|
332
|
+
target: compactReferenceTarget(payload.target),
|
|
288
333
|
meta: payload.meta,
|
|
289
334
|
targetPath:
|
|
290
|
-
typeof payload.
|
|
335
|
+
typeof payload.target?.path === "string"
|
|
336
|
+
? payload.target.path
|
|
337
|
+
: typeof payload.meta?.targetPath === "string"
|
|
338
|
+
? payload.meta.targetPath
|
|
339
|
+
: undefined,
|
|
291
340
|
sourcePage,
|
|
292
341
|
};
|
|
293
342
|
default:
|
package/src/lib/theme-runtime.ts
CHANGED
|
@@ -39,6 +39,18 @@ function Trigger() {
|
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function PreserveNavigationTrigger() {
|
|
43
|
+
const handleNewConversation = useNewConversation({
|
|
44
|
+
navigateToChat: false,
|
|
45
|
+
preserveNavigation: true,
|
|
46
|
+
});
|
|
47
|
+
return (
|
|
48
|
+
<button type="button" onClick={handleNewConversation}>
|
|
49
|
+
New Bubble Thread
|
|
50
|
+
</button>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
describe("useNewConversation", () => {
|
|
43
55
|
let container: HTMLDivElement;
|
|
44
56
|
let root: Root;
|
|
@@ -121,4 +133,30 @@ describe("useNewConversation", () => {
|
|
|
121
133
|
expect(citationStore.getState().results).toEqual([]);
|
|
122
134
|
expect(citationStore.getState().handler).toBeNull();
|
|
123
135
|
});
|
|
136
|
+
|
|
137
|
+
it("can reset the thread without leaving the current page", async () => {
|
|
138
|
+
await act(async () => {
|
|
139
|
+
root.render(
|
|
140
|
+
<MemoryRouter initialEntries={["/spaces/demo"]}>
|
|
141
|
+
<PreserveNavigationTrigger />
|
|
142
|
+
<LocationProbe />
|
|
143
|
+
</MemoryRouter>,
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const button = container.querySelector("button");
|
|
148
|
+
expect(button).toBeTruthy();
|
|
149
|
+
|
|
150
|
+
await act(async () => {
|
|
151
|
+
button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(container.querySelector('[data-testid="location"]')?.textContent).toBe("/spaces/demo");
|
|
155
|
+
expect(switchToNewThread).toHaveBeenCalledTimes(1);
|
|
156
|
+
expect(resetThread).toHaveBeenCalledTimes(1);
|
|
157
|
+
expect(useAppStore.getState().currentPage).toBe("/spaces/demo");
|
|
158
|
+
expect(useAppStore.getState().navContext.label).toBe("Demo");
|
|
159
|
+
expect(useAppStore.getState().threadActive).toBe(false);
|
|
160
|
+
expect(useAppStore.getState().contextItems).toEqual([]);
|
|
161
|
+
});
|
|
124
162
|
});
|
|
@@ -12,7 +12,7 @@ import { useSidebarStore } from "./sidebar-store.ts";
|
|
|
12
12
|
* Shared hook for starting a new conversation.
|
|
13
13
|
* Used by ChatHeader and Sidebar to keep behavior in sync.
|
|
14
14
|
*/
|
|
15
|
-
export function useNewConversation() {
|
|
15
|
+
export function useNewConversation(options?: { navigateToChat?: boolean; preserveNavigation?: boolean }) {
|
|
16
16
|
const aui = useAui();
|
|
17
17
|
const { resetThread } = useAGUIAdapter();
|
|
18
18
|
const navigate = useNavigate();
|
|
@@ -22,12 +22,33 @@ export function useNewConversation() {
|
|
|
22
22
|
const clearCitations = useStore(citationStore, (s) => s.clear);
|
|
23
23
|
|
|
24
24
|
return useCallback(() => {
|
|
25
|
+
const preserved = options?.preserveNavigation
|
|
26
|
+
? {
|
|
27
|
+
currentPage: useAppStore.getState().currentPage,
|
|
28
|
+
navContext: useAppStore.getState().navContext,
|
|
29
|
+
}
|
|
30
|
+
: null;
|
|
25
31
|
closePanel();
|
|
26
32
|
resetApp();
|
|
33
|
+
if (preserved) {
|
|
34
|
+
useAppStore.setState(preserved);
|
|
35
|
+
}
|
|
27
36
|
closeMobile();
|
|
28
37
|
clearCitations();
|
|
29
38
|
resetThread();
|
|
30
39
|
aui.threads().switchToNewThread();
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
if (options?.navigateToChat ?? true) {
|
|
41
|
+
navigate("/");
|
|
42
|
+
}
|
|
43
|
+
}, [
|
|
44
|
+
closePanel,
|
|
45
|
+
resetApp,
|
|
46
|
+
options?.preserveNavigation,
|
|
47
|
+
closeMobile,
|
|
48
|
+
clearCitations,
|
|
49
|
+
resetThread,
|
|
50
|
+
aui,
|
|
51
|
+
options?.navigateToChat,
|
|
52
|
+
navigate,
|
|
53
|
+
]);
|
|
33
54
|
}
|