@iloveagents/foundry-web-ui 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @iloveagents/foundry-web-ui
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 9ec870e: Surface AG-UI run errors in chat instead of rendering an empty assistant bubble.
8
+ - @iloveagents/foundry-agent@0.3.1
9
+ - @iloveagents/foundry-web-primitives@0.3.1
10
+
11
+ ## 0.3.0
12
+
13
+ ### Patch Changes
14
+
15
+ - @iloveagents/foundry-agent@0.3.0
16
+ - @iloveagents/foundry-web-primitives@0.3.0
17
+
3
18
  ## 0.2.2
4
19
 
5
20
  ### Patch Changes
package/README.md CHANGED
@@ -36,6 +36,6 @@ Feature modules (like SPACES) plug into `@iloveagents/foundry-web-ui` via regist
36
36
  - **NavItem.dnd** — drag-and-drop behavior on sidebar items
37
37
  - **Panel renderer registry** — custom content renderers for the side panel
38
38
  - **Citation handler** — handles `[n]` citation clicks in markdown
39
- - **Tool UI** — custom rendering for agent tools via `useAssistantToolUI`
39
+ - **Tool UI** — custom rendering for agent tools via `makeAssistantToolUI`
40
40
 
41
41
  See [AGENTS.md](./AGENTS.md) for architecture details and import boundary rules.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-web-ui",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "types": "./src/index.ts",
@@ -46,8 +46,8 @@
46
46
  "react-markdown": "^10.0.0",
47
47
  "remark-gfm": "^4.0.0",
48
48
  "shiki": "^4.0.0",
49
- "@iloveagents/foundry-agent": "0.2.2",
50
- "@iloveagents/foundry-web-primitives": "0.2.2"
49
+ "@iloveagents/foundry-agent": "0.3.1",
50
+ "@iloveagents/foundry-web-primitives": "0.3.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "typescript": "~5.9.3",
@@ -1407,6 +1407,7 @@ function SidebarContent({ collapsed }: { collapsed: boolean }) {
1407
1407
  const toggle = useSidebarStore((s) => s.toggle);
1408
1408
  const handleNewConversation = useNewConversation();
1409
1409
  const rawNavConfig = useNavStore((s) => s.config);
1410
+ const navLoading = useNavStore((s) => s.navLoading);
1410
1411
  // Sort by `priority` descending so feature hooks can declare slot
1411
1412
  // intent (e.g. "Recents above Tasks") instead of relying on the
1412
1413
  // order in which addGroup() happened to be called. Stable sort: ties
@@ -1420,6 +1421,12 @@ function SidebarContent({ collapsed }: { collapsed: boolean }) {
1420
1421
  .map(({ g }) => g),
1421
1422
  [rawNavConfig],
1422
1423
  );
1424
+ // Show skeleton rows during the cold nav fetch — but only until the
1425
+ // integrator's primary dynamic group ("Workspaces") has been published.
1426
+ // Once it exists, real rows render even if a later phase (status dots) is
1427
+ // still resolving, so the skeleton never flashes over a populated tree.
1428
+ const hasWorkspacesGroup = navConfig.some((group) => group.label === "Workspaces");
1429
+ const showNavSkeleton = navLoading && !hasWorkspacesGroup;
1423
1430
  const currentPage = useAppStore((s) => s.currentPage);
1424
1431
  const navContext = useAppStore((s) => s.navContext);
1425
1432
 
@@ -1546,6 +1553,16 @@ function SidebarContent({ collapsed }: { collapsed: boolean }) {
1546
1553
  {navConfig.map((group) => (
1547
1554
  <NavGroupSection key={group.label} group={group} />
1548
1555
  ))}
1556
+ {showNavSkeleton && (
1557
+ <div className="space-y-1 px-1 pt-2" aria-hidden="true">
1558
+ {Array.from({ length: 6 }).map((_, index) => (
1559
+ <div
1560
+ key={index}
1561
+ className="h-7 mx-1 rounded-lg animate-pulse bg-sidebar-accent/40"
1562
+ />
1563
+ ))}
1564
+ </div>
1565
+ )}
1549
1566
  </nav>
1550
1567
 
1551
1568
  {/* Footer actions — registered by feature modules */}
@@ -4,10 +4,11 @@
4
4
  * The shim is a generator-driven assistant-ui `ChatModelAdapter` wrapping
5
5
  * `@iloveagents/foundry-agent`'s `AGUIRunner`. The contract under test:
6
6
  * - Every intermediate yield carries `status: { type: "running" }`.
7
- * - The FINAL yield carries `status: { type: "complete", reason: "stop" }`
8
- * even after a multi-turn client-tool follow-upthe previous adapter
9
- * left this status as `running` and the message bubble got stuck in
10
- * the loading state. See PR #116 review.
7
+ * - The FINAL yield settles the message so the bubble unsticks (the
8
+ * previous adapter left it `running` and it got stuck loading PR #116):
9
+ * `{ type: "complete", reason: "stop" }` on success, or
10
+ * `{ type: "incomplete", reason: "error", error }` on RUN_ERROR so the
11
+ * UI shows a visible error instead of a silent empty bubble.
11
12
  * - `content` is cumulative — every yield holds the full content array,
12
13
  * not deltas (assistant-ui v0.12 contract).
13
14
  */
@@ -170,7 +171,7 @@ describe("AGUIAdapterSDK", () => {
170
171
  expect(finalContent.find((p) => p.type === "text")?.text).toBe("Done");
171
172
  });
172
173
 
173
- it("yields a terminal complete on run-error (does not throw)", async () => {
174
+ it("settles run-error as incomplete/error so the UI shows it (does not throw)", async () => {
174
175
  runMock.mockImplementation(async function* (): AsyncGenerator<RunnerEvent> {
175
176
  yield { type: "turn-started" };
176
177
  yield { type: "request-sent", input: {} };
@@ -181,15 +182,20 @@ describe("AGUIAdapterSDK", () => {
181
182
 
182
183
  const adapter = new AGUIAdapterSDK();
183
184
  const out: ChatModelRunResultLike[] = [];
184
- // The shim swallows run errors and emits a terminal complete so the
185
- // bubble unsticks. Throwing here would re-introduce the stuck-running
186
- // regression because assistant-ui treats unhandled throws as the
187
- // message being abandoned mid-run.
185
+ // On RUN_ERROR the adapter settles the message as incomplete/error
186
+ // (NOT a silent complete/stop) so assistant-ui's <MessagePrimitive.Error>
187
+ // renders a visible, retryable error. It must still settle via a
188
+ // terminal yield rather than throw — an unhandled throw re-introduces
189
+ // the stuck-running regression (message abandoned mid-run).
188
190
  for await (const r of adapter.run(makeRunInput())) {
189
191
  out.push({ status: r.status, content: r.content });
190
192
  }
191
193
 
192
- expect(out[out.length - 1].status).toEqual({ type: "complete", reason: "stop" });
194
+ expect(out[out.length - 1].status).toEqual({
195
+ type: "incomplete",
196
+ reason: "error",
197
+ error: "boom",
198
+ });
193
199
  });
194
200
 
195
201
  it("forwards app context items through the native AG-UI context field", async () => {
@@ -243,13 +243,27 @@ export class AGUIAdapterSDK implements ChatModelAdapter {
243
243
 
244
244
  // Terminal yield — runner generator returned. assistant-ui leaves
245
245
  // the message stuck in the loading state if the last yield's
246
- // `status` is `running`, so we always emit a final `complete` once
247
- // the runner is fully done (after every turn + every follow-up).
248
- // `runError` is recorded above but not re-thrown: the terminal yield
249
- // settles the message with reason=stop so the bubble unsticks; an
250
- // unhandled throw would re-introduce the stuck-running bug.
251
- void runError;
252
- yield buildResult({ type: "complete", reason: "stop" });
246
+ // `status` is `running`, so we always emit a final settle once the
247
+ // runner is fully done (after every turn + every follow-up).
248
+ //
249
+ // If the run errored (the AG-UI backend emitted RUN_ERROR — e.g. the
250
+ // agent exhausted rate-limit/transient retries, or a non-retryable
251
+ // 4xx), settle as `incomplete`/`error` instead of `complete`/`stop`,
252
+ // carrying the error message. This keeps any partial content AND
253
+ // flips the message into assistant-ui's error state so the existing
254
+ // `<MessagePrimitive.Error>` renders a visible, retryable error —
255
+ // not a silent empty bubble. We yield the terminal status rather
256
+ // than throwing; an unhandled throw re-introduces the stuck-running
257
+ // bug (the runner generator has already returned here).
258
+ if (runError !== null) {
259
+ yield buildResult({
260
+ type: "incomplete",
261
+ reason: "error",
262
+ error: runError || "The run failed.",
263
+ });
264
+ } else {
265
+ yield buildResult({ type: "complete", reason: "stop" });
266
+ }
253
267
  } finally {
254
268
  streamingStatusStore.getState().setStreamingStatus({ status: "idle" });
255
269
  }
@@ -24,8 +24,18 @@ export interface NavFooterAction {
24
24
  interface NavStoreState {
25
25
  config: NavGroup[];
26
26
  footerActions: NavFooterAction[];
27
+ /**
28
+ * True while an integrator's nav sync is loading its first batch of
29
+ * groups. Optional/defaulted (starts ``false``) so existing consumers
30
+ * that never set it are unaffected. The sidebar reads it to render
31
+ * skeleton rows instead of an empty list during the cold fetch —
32
+ * avoids the "empty sidebar for N seconds" gap on a slow nav sync.
33
+ */
34
+ navLoading: boolean;
27
35
  /** Replace the entire config */
28
36
  setConfig: (config: NavGroup[]) => void;
37
+ /** Toggle the cold-load skeleton state. */
38
+ setNavLoading: (loading: boolean) => void;
29
39
  /** Add a group (replaces existing group with the same label) */
30
40
  addGroup: (group: NavGroup) => void;
31
41
  /** Remove a group by label */
@@ -39,9 +49,12 @@ interface NavStoreState {
39
49
  export const useNavStore = create<NavStoreState>((set, get) => ({
40
50
  config: DEFAULT_NAV_CONFIG,
41
51
  footerActions: [],
52
+ navLoading: false,
42
53
 
43
54
  setConfig: (config) => set({ config }),
44
55
 
56
+ setNavLoading: (navLoading) => set({ navLoading }),
57
+
45
58
  addGroup: (group) => {
46
59
  const config = get().config;
47
60
  const idx = config.findIndex((g) => g.label === group.label);