@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 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.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.1",
27
- "@iloveagents/foundry-web-primitives": "0.1.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 || "LastSpace.ai";
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
- /** Starter prompts shown on empty state. Customize these for your agent's capabilities. */
227
- const STARTER_SUGGESTIONS = [
228
- "Show me a short document about Python",
229
- "Summarize what you can help me with here",
230
- "Draft a concise release note from bullet points",
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
- <div className="flex flex-wrap justify-center gap-2 max-w-lg">
361
- {STARTER_SUGGESTIONS.map((prompt) => (
362
- <ThreadPrimitive.Suggestion key={prompt} prompt={prompt} send asChild>
363
- <Button variant="outline" className="text-sm">
364
- {prompt}
365
- </Button>
366
- </ThreadPrimitive.Suggestion>
367
- ))}
368
- </div>
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 || "LastSpace.ai";
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: { contextItems: [] as Array<Record<string, unknown>> },
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: [{ id: "u-1", role: "user" as const, content: [{ type: "text" as const, text: "hi" }] }],
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 = { contextItems: [] };
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", () => {
@@ -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
- if (typeof item.targetPath === "string") {
282
- return `Selected ${refType} atom. Use targetPath as the exact edit target when the user asks to modify, rewrite, or transform selected content.`;
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
  *
@@ -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.meta?.targetPath === "string" ? payload.meta.targetPath : undefined;
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.meta?.targetPath === "string" ? payload.meta.targetPath : undefined,
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:
@@ -595,8 +595,8 @@ export const FOUNDRY_VIOLET_THEME: ThemeDefinition = {
595
595
  radius: "0.875rem",
596
596
  },
597
597
  branding: {
598
- appName: "LastSpace.ai",
599
- appTitle: "LastSpace.ai",
598
+ appName: "eomi",
599
+ appTitle: "eomi",
600
600
  },
601
601
  };
602
602
 
@@ -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
- navigate("/");
32
- }, [closePanel, resetApp, closeMobile, clearCitations, resetThread, aui, navigate]);
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
  }