@iloveagents/foundry-web-ui 0.4.0 → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useRef, useCallback } from "react";
3
- import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, BranchPickerPrimitive, ErrorPrimitive, useAuiState, useComposerRuntime, useThreadViewport, } from "@assistant-ui/react";
3
+ import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, BranchPickerPrimitive, useAuiState, useComposerRuntime, useThreadViewport, } from "@assistant-ui/react";
4
4
  import { ArrowUp, Copy, Check, RefreshCw, PencilIcon, ChevronLeft, ChevronRight, ChevronDown, Square, Bot, Paperclip, Minimize2, PanelRightClose, PanelRightOpen, } from "lucide-react";
5
5
  import { cn } from "@iloveagents/foundry-web-primitives";
6
6
  import { Button } from "@iloveagents/foundry-web-primitives";
@@ -13,18 +13,19 @@ import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
13
13
  import { MarkdownText } from "./markdown-text.js";
14
14
  import { ToolFallback } from "./tool-fallback.js";
15
15
  import { LoadingIndicator, LoadingBar } from "./loading-indicator.js";
16
+ import { MessageTimestamp } from "./message-timestamp.js";
17
+ import { MessageError } from "./message-error.js";
16
18
  const BranchPicker = () => (_jsxs(BranchPickerPrimitive.Root, { hideWhenSingleBranch: true, className: "inline-flex items-center gap-1 text-xs", children: [_jsx(BranchPickerPrimitive.Previous, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Previous", size: "icon", children: _jsx(ChevronLeft, { className: "size-4" }) }) }), _jsxs("span", { className: "text-muted-foreground", children: [_jsx(BranchPickerPrimitive.Number, {}), " / ", _jsx(BranchPickerPrimitive.Count, {})] }), _jsx(BranchPickerPrimitive.Next, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Next", size: "icon", children: _jsx(ChevronRight, { className: "size-4" }) }) })] }));
17
19
  const UserMessage = () => {
18
20
  const messageId = useAuiState((s) => s.message.id);
19
- return (_jsx(MessagePrimitive.Root, { className: "w-full mb-2 group", children: _jsxs("div", { className: "flex flex-col items-end gap-1", children: [_jsx(MessagePrimitive.Attachments, { components: userAttachmentComponents }), _jsx(SentContextBadges, { messageId: messageId }), _jsx("div", { className: cn("max-w-[80%]", "bg-muted rounded-2xl", "px-4 py-3", "text-foreground"), children: _jsx(MessagePrimitive.Content, {}) }), _jsxs("div", { className: "flex h-8 items-center justify-end gap-2 pr-2", children: [_jsx(BranchPicker, {}), _jsx("div", { className: "flex h-8 w-10 items-center justify-end", children: _jsx(UserActionBar, {}) })] })] }) }));
21
+ return (_jsx(MessagePrimitive.Root, { className: "w-full mb-2 group", children: _jsxs("div", { className: "flex flex-col items-end gap-1", children: [_jsx(MessagePrimitive.Attachments, { components: userAttachmentComponents }), _jsx(SentContextBadges, { messageId: messageId }), _jsx("div", { className: cn("max-w-[80%]", "bg-muted rounded-2xl", "px-4 py-3", "text-foreground"), children: _jsx(MessagePrimitive.Content, {}) }), _jsxs("div", { className: "flex h-8 items-center justify-end gap-2 pr-2", children: [_jsx(MessageTimestamp, {}), _jsx(BranchPicker, {}), _jsx("div", { className: "flex h-8 w-10 items-center justify-end", children: _jsx(UserActionBar, {}) })] })] }) }));
20
22
  };
21
23
  const UserActionBar = () => (_jsx(ActionBarPrimitive.Root, { hideWhenRunning: true, className: "flex items-center opacity-0 group-hover:opacity-100 transition-opacity", children: _jsx(ActionBarPrimitive.Edit, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Edit", size: "icon", children: _jsx(PencilIcon, { className: "size-4" }) }) }) }));
22
24
  const AssistantMessage = () => (_jsx(MessagePrimitive.Root, { className: "w-full mb-2 group", children: _jsxs("div", { className: "mx-auto max-w-3xl px-4", children: [_jsx("div", { className: "text-foreground leading-relaxed flex flex-col gap-4", children: _jsx(MessagePrimitive.Parts, { components: {
23
25
  Text: MarkdownText,
24
26
  tools: { Fallback: ToolFallback },
25
- } }) }), _jsx(MessageError, {}), _jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("div", { className: "flex h-8 w-[4.25rem] items-center", children: _jsx(AssistantActionBar, {}) }), _jsx(BranchPicker, {})] })] }) }));
27
+ } }) }), _jsx(MessageError, {}), _jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("div", { className: "flex h-8 w-[4.25rem] items-center", children: _jsx(AssistantActionBar, {}) }), _jsx(BranchPicker, {}), _jsx(MessageTimestamp, {})] })] }) }));
26
28
  const AssistantActionBar = () => (_jsxs(ActionBarPrimitive.Root, { hideWhenRunning: true, className: cn("pointer-events-none flex items-center gap-1 -ml-2 opacity-0 transition-opacity", "group-hover:pointer-events-auto group-hover:opacity-100", "group-focus-within:pointer-events-auto group-focus-within:opacity-100"), children: [_jsx(ActionBarPrimitive.Copy, { asChild: true, children: _jsxs(TooltipIconButton, { tooltip: "Copy", size: "icon", children: [_jsx(MessagePrimitive.If, { copied: false, children: _jsx(Copy, { className: "size-4" }) }), _jsx(MessagePrimitive.If, { copied: true, children: _jsx(Check, { className: "size-4 text-primary" }) })] }) }), _jsx(ActionBarPrimitive.Reload, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Refresh", size: "icon", children: _jsx(RefreshCw, { className: "size-4" }) }) })] }));
27
- const MessageError = () => (_jsx(MessagePrimitive.Error, { children: _jsx(ErrorPrimitive.Root, { className: "mt-2 rounded-lg border border-destructive bg-destructive/10 p-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium text-destructive", children: "Error:" }), _jsx(ErrorPrimitive.Message, { className: "text-sm text-destructive" })] }) }) }));
28
29
  const EditComposer = () => (_jsx(MessagePrimitive.Root, { className: "w-full mb-2", children: _jsx("div", { className: "flex items-start justify-end", children: _jsx("div", { className: "w-full max-w-[80%]", children: _jsxs(ComposerPrimitive.Root, { className: cn("flex flex-col gap-2", "rounded-2xl border border-input", "bg-background shadow-sm", "px-4 py-3"), children: [_jsx(ComposerPrimitive.Input, { autoFocus: true, className: cn("w-full bg-transparent", "text-base text-foreground", "outline-none resize-none", "placeholder:text-muted-foreground") }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "sm", children: "Cancel" }) }), _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "sm", children: "Update" }) })] })] }) }) }) }));
29
30
  const ComposerActionButton = () => {
30
31
  return (_jsxs(_Fragment, { children: [_jsx(ThreadPrimitive.If, { running: true, children: _jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon", className: "rounded-full size-10 flex-shrink-0", "aria-label": "Stop generating", children: _jsx(Square, { className: "size-4" }) }) }) }), _jsx(ThreadPrimitive.If, { running: false, children: _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "icon", className: "rounded-full size-10 flex-shrink-0", "aria-label": "Send message", children: _jsx(ArrowUp, { className: "size-5" }) }) }) })] }));
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useRef, useState } from "react";
3
3
  import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, useAuiState, useThreadViewport, } from "@assistant-ui/react";
4
- import { MessageSquare, X, Maximize2, SquarePen, ArrowUp, Square, ChevronDown, Copy, Check, Paperclip, } from "lucide-react";
4
+ import { MessageSquare, X, Maximize2, SquarePen, ArrowUp, Square, ChevronDown, Copy, Check, Paperclip, RefreshCw, } from "lucide-react";
5
5
  import { cn } from "@iloveagents/foundry-web-primitives";
6
6
  import { Button } from "@iloveagents/foundry-web-primitives";
7
7
  import { TooltipIconButton } from "./tooltip-icon-button.js";
@@ -10,6 +10,8 @@ import { ComposerContextBadges, SentContextBadges } from "./context-badges.js";
10
10
  import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
11
11
  import { MarkdownText } from "./markdown-text.js";
12
12
  import { LoadingIndicator } from "./loading-indicator.js";
13
+ import { MessageTimestamp } from "./message-timestamp.js";
14
+ import { MessageError } from "./message-error.js";
13
15
  import { useAppStore } from "../lib/app-store.js";
14
16
  import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
15
17
  import { useNewConversation } from "../lib/use-new-conversation.js";
@@ -18,7 +20,7 @@ const CHAT_BUBBLE_INSET = 8;
18
20
  function BubbleAssistantMessage() {
19
21
  return (_jsxs(MessagePrimitive.Root, { className: "w-full mb-2 group", children: [_jsx("div", { className: "text-foreground text-sm leading-relaxed flex flex-col gap-2", children: _jsx(MessagePrimitive.Parts, { components: {
20
22
  Text: MarkdownText,
21
- } }) }), _jsx("div", { className: "flex h-7 w-8 items-center mt-1", children: _jsx(ActionBarPrimitive.Root, { hideWhenRunning: true, className: cn("pointer-events-none flex items-center gap-1 opacity-0 transition-opacity", "group-hover:pointer-events-auto group-hover:opacity-100", "group-focus-within:pointer-events-auto group-focus-within:opacity-100"), children: _jsx(ActionBarPrimitive.Copy, { asChild: true, children: _jsxs(TooltipIconButton, { tooltip: "Copy", size: "icon", className: "size-7", children: [_jsx(MessagePrimitive.If, { copied: false, children: _jsx(Copy, { className: "size-3" }) }), _jsx(MessagePrimitive.If, { copied: true, children: _jsx(Check, { className: "size-3 text-primary" }) })] }) }) }) })] }));
23
+ } }) }), _jsx(MessageError, { compact: true }), _jsxs("div", { className: "flex h-7 items-center gap-2 mt-1", children: [_jsxs(ActionBarPrimitive.Root, { hideWhenRunning: true, className: cn("pointer-events-none flex items-center gap-1 opacity-0 transition-opacity", "group-hover:pointer-events-auto group-hover:opacity-100", "group-focus-within:pointer-events-auto group-focus-within:opacity-100"), children: [_jsx(ActionBarPrimitive.Copy, { asChild: true, children: _jsxs(TooltipIconButton, { tooltip: "Copy", size: "icon", className: "size-7", children: [_jsx(MessagePrimitive.If, { copied: false, children: _jsx(Copy, { className: "size-3" }) }), _jsx(MessagePrimitive.If, { copied: true, children: _jsx(Check, { className: "size-3 text-primary" }) })] }) }), _jsx(ActionBarPrimitive.Reload, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Retry", size: "icon", className: "size-7", children: _jsx(RefreshCw, { className: "size-3" }) }) })] }), _jsx(MessageTimestamp, {})] })] }));
22
24
  }
23
25
  function BubbleUserMessage() {
24
26
  const messageId = useAuiState((s) => s.message.id);
@@ -1,4 +1,13 @@
1
1
  /** Shimmer bar shown above the composer while the agent is running. */
2
2
  export declare function LoadingBar(): import("react/jsx-runtime").JSX.Element;
3
- /** Inline status text shown in the message area while the agent is running. */
3
+ /**
4
+ * Inline status shown in the message area while the agent is running.
5
+ *
6
+ * Visible for the whole run (assistant-ui `running` gate) with the current
7
+ * phase, plus a distinct amber "stalled" treatment when no event or server
8
+ * heartbeat has arrived for a while — so a user can always tell "still
9
+ * working" from "connection is dead". A dead stream is additionally
10
+ * surfaced as an explicit run error by the runner; this indicator covers
11
+ * the in-flight phases.
12
+ */
4
13
  export declare function LoadingIndicator(): import("react/jsx-runtime").JSX.Element;
@@ -8,15 +8,28 @@ function formatToolName(name) {
8
8
  export function LoadingBar() {
9
9
  return (_jsx("div", { className: "w-full h-px bg-primary/30 overflow-hidden", children: _jsx("div", { className: "h-full w-1/4 bg-primary animate-[shimmer_1.5s_ease-in-out_infinite]" }) }));
10
10
  }
11
- /** Inline status text shown in the message area while the agent is running. */
11
+ /**
12
+ * Inline status shown in the message area while the agent is running.
13
+ *
14
+ * Visible for the whole run (assistant-ui `running` gate) with the current
15
+ * phase, plus a distinct amber "stalled" treatment when no event or server
16
+ * heartbeat has arrived for a while — so a user can always tell "still
17
+ * working" from "connection is dead". A dead stream is additionally
18
+ * surfaced as an explicit run error by the runner; this indicator covers
19
+ * the in-flight phases.
20
+ */
12
21
  export function LoadingIndicator() {
13
22
  const streaming = useStore(streamingStatusStore, (s) => s.streamingStatus);
23
+ const stalled = streaming.status === "stalled";
14
24
  let label = "Thinking";
15
- if (streaming.status === "calling" && streaming.toolName) {
25
+ if (stalled) {
26
+ label = "Still working — no signal from the agent";
27
+ }
28
+ else if (streaming.status === "calling" && streaming.toolName) {
16
29
  label = `Running ${formatToolName(streaming.toolName)}`;
17
30
  }
18
31
  else if (streaming.status === "streaming") {
19
32
  label = "Generating response";
20
33
  }
21
- return (_jsx("div", { className: "h-10 px-4", children: _jsxs("div", { className: "flex items-center gap-2 py-2 text-foreground leading-relaxed animate-in fade-in duration-300", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: label }), _jsxs("div", { className: "flex gap-1", children: [_jsx("span", { className: "size-1.5 rounded-full bg-primary/60 animate-[pulse_1.4s_ease-in-out_infinite]" }), _jsx("span", { className: "size-1.5 rounded-full bg-primary/60 animate-[pulse_1.4s_ease-in-out_0.2s_infinite]" }), _jsx("span", { className: "size-1.5 rounded-full bg-primary/60 animate-[pulse_1.4s_ease-in-out_0.4s_infinite]" })] })] }) }));
34
+ return (_jsx("div", { className: "h-10 px-4", children: _jsxs("div", { className: "flex items-center gap-2 py-2 text-foreground leading-relaxed animate-in fade-in duration-300", children: [_jsx("span", { className: stalled ? "text-sm text-warning" : "text-sm text-muted-foreground", children: label }), _jsxs("div", { className: "flex gap-1", children: [_jsx("span", { className: `size-1.5 rounded-full ${stalled ? "bg-warning/70" : "bg-primary/60"} animate-[pulse_1.4s_ease-in-out_infinite]` }), _jsx("span", { className: `size-1.5 rounded-full ${stalled ? "bg-warning/70" : "bg-primary/60"} animate-[pulse_1.4s_ease-in-out_0.2s_infinite]` }), _jsx("span", { className: `size-1.5 rounded-full ${stalled ? "bg-warning/70" : "bg-primary/60"} animate-[pulse_1.4s_ease-in-out_0.4s_infinite]` })] })] }) }));
22
35
  }
@@ -0,0 +1,10 @@
1
+ import type { FC } from "react";
2
+ /**
3
+ * Visible, styled run-error for an assistant message — rendered whenever the
4
+ * message settled with an error status (agent RUN_ERROR, lost connection,
5
+ * transport failure). Shared by the full-page thread and the compact chat
6
+ * bubble so a failed run is NEVER silent in either surface.
7
+ */
8
+ export declare const MessageError: FC<{
9
+ compact?: boolean;
10
+ }>;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { MessagePrimitive, ErrorPrimitive } from "@assistant-ui/react";
3
+ import { cn } from "@iloveagents/foundry-web-primitives";
4
+ /**
5
+ * Visible, styled run-error for an assistant message — rendered whenever the
6
+ * message settled with an error status (agent RUN_ERROR, lost connection,
7
+ * transport failure). Shared by the full-page thread and the compact chat
8
+ * bubble so a failed run is NEVER silent in either surface.
9
+ */
10
+ export const MessageError = ({ compact }) => (_jsx(MessagePrimitive.Error, { children: _jsx(ErrorPrimitive.Root, { className: cn("mt-2 rounded-lg border border-destructive bg-destructive/10", compact ? "p-2" : "p-3"), children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: cn("font-medium text-destructive", compact ? "text-xs" : "text-sm"), children: "Error:" }), _jsx(ErrorPrimitive.Message, { className: cn("text-destructive", compact ? "text-xs" : "text-sm") })] }) }) }));
@@ -0,0 +1,10 @@
1
+ import type { FC } from "react";
2
+ /**
3
+ * Message send/receive time — revealed on hover in the same row as the
4
+ * message actions (edit / copy). The short time is shown; the full date
5
+ * lives in the native tooltip. Shared by the full-page thread and the
6
+ * compact chat bubble.
7
+ */
8
+ export declare const MessageTimestamp: FC<{
9
+ className?: string;
10
+ }>;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useAuiState } from "@assistant-ui/react";
3
+ import { cn } from "@iloveagents/foundry-web-primitives";
4
+ /**
5
+ * Message send/receive time — revealed on hover in the same row as the
6
+ * message actions (edit / copy). The short time is shown; the full date
7
+ * lives in the native tooltip. Shared by the full-page thread and the
8
+ * compact chat bubble.
9
+ */
10
+ export const MessageTimestamp = ({ className }) => {
11
+ const createdAt = useAuiState((s) => s.message.createdAt);
12
+ if (!createdAt)
13
+ return null;
14
+ const d = createdAt instanceof Date ? createdAt : new Date(createdAt);
15
+ if (Number.isNaN(d.getTime()))
16
+ return null;
17
+ return (_jsx("span", { title: d.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }), className: cn("text-xs text-muted-foreground/70 tabular-nums select-none", "opacity-0 transition-opacity group-hover:opacity-100", className), children: d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }) }));
18
+ };
@@ -48,6 +48,10 @@ export class AGUIAdapterSDK {
48
48
  let currentText = "";
49
49
  const toolCalls = new Map();
50
50
  let runError = null;
51
+ // Liveness clock for the working indicator: every runner event (incl.
52
+ // server heartbeats) proves the run is alive; the indicator derives
53
+ // elapsed time from runStartedAt and staleness from lastSignalAt.
54
+ streamingStatusStore.getState().markRunStarted();
51
55
  /**
52
56
  * Build a `ChatModelRunResult` from the accumulated state.
53
57
  *
@@ -103,7 +107,11 @@ export class AGUIAdapterSDK {
103
107
  abortSignal,
104
108
  })) {
105
109
  let shouldYield = false;
110
+ streamingStatusStore.getState().touchSignal();
106
111
  switch (event.type) {
112
+ case "heartbeat":
113
+ // Server liveness ping — already recorded via touchSignal above.
114
+ break;
107
115
  case "request-sent":
108
116
  if (import.meta.env.DEV) {
109
117
  useDevStore.getState().captureRequest(event.input);
@@ -226,6 +234,7 @@ export class AGUIAdapterSDK {
226
234
  }
227
235
  finally {
228
236
  streamingStatusStore.getState().setStreamingStatus({ status: "idle" });
237
+ streamingStatusStore.getState().markRunEnded();
229
238
  }
230
239
  }
231
240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-web-ui",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,8 +53,8 @@
53
53
  "tailwind-merge": "^3.5.0",
54
54
  "react-markdown": "^10.0.0",
55
55
  "remark-gfm": "^4.0.0",
56
- "@iloveagents/foundry-agent": "^0.4.0",
57
- "@iloveagents/foundry-web-primitives": "^0.4.0"
56
+ "@iloveagents/foundry-agent": "^0.5.0",
57
+ "@iloveagents/foundry-web-primitives": "^0.5.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@ag-ui/client": "^0.0.52",