@tangle-network/ui 11.51.2 → 11.51.4

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,15 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.51.4
4
+
5
+ - Normalize retained record lifecycle aliases through the shared status vocabulary; stopped and absent terminal states remain distinct from success.
6
+
7
+ ## 11.51.3
8
+
9
+ ### Patch Changes
10
+
11
+ - Share the native event-stream lifecycle and measured-content following between existing UI chat hooks and retained-run consumers; preserve buffered-stream retries, authenticated host transports and existing exports.
12
+
3
13
  ## 11.51.2
4
14
 
5
15
  ### Patch Changes
@@ -1,8 +1,5 @@
1
1
  import { RefObject } from "react";
2
- /**
3
- * Scroll-to-bottom behaviour: sticks to the bottom while streaming,
4
- * pauses when user scrolls up, resumes when user scrolls back down.
5
- */
2
+ /** Existing chat contract, backed by the shared measured-content following lifecycle. */
6
3
  export declare function useAutoScroll(containerRef: RefObject<HTMLElement | null>, deps?: unknown[]): {
7
4
  isAtBottom: boolean;
8
5
  scrollToBottom: () => void;
@@ -1,47 +1,13 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import { useFollowTail } from "./use-follow-tail.js";
2
+ import { useCallback, useMemo } from "react";
2
3
  // src/hooks/use-auto-scroll.ts
3
- const BOTTOM_THRESHOLD = 40;
4
- /**
5
- * Scroll-to-bottom behaviour: sticks to the bottom while streaming,
6
- * pauses when user scrolls up, resumes when user scrolls back down.
7
- */
4
+ /** Existing chat contract, backed by the shared measured-content following lifecycle. */
8
5
  function useAutoScroll(containerRef, deps = []) {
9
- const [isAtBottom, setIsAtBottom] = useState(true);
10
- const userScrolledUp = useRef(false);
11
- const checkBottom = useCallback(() => {
12
- const el = containerRef.current;
13
- if (!el) return true;
14
- return el.scrollHeight - el.scrollTop - el.clientHeight < BOTTOM_THRESHOLD;
15
- }, [containerRef]);
16
- useEffect(() => {
17
- const el = containerRef.current;
18
- if (!el) return;
19
- const onScroll = () => {
20
- const atBottom = checkBottom();
21
- setIsAtBottom(atBottom);
22
- userScrolledUp.current = !atBottom;
23
- };
24
- el.addEventListener("scroll", onScroll, { passive: true });
25
- return () => el.removeEventListener("scroll", onScroll);
26
- }, [containerRef, checkBottom]);
27
- useEffect(() => {
28
- if (userScrolledUp.current) return;
29
- const el = containerRef.current;
30
- if (!el) return;
31
- el.scrollTop = el.scrollHeight;
32
- }, deps);
6
+ const content = useMemo(() => ({}), deps);
7
+ const { atBottom, jump } = useFollowTail(containerRef, { content });
33
8
  return {
34
- isAtBottom,
35
- scrollToBottom: useCallback(() => {
36
- const el = containerRef.current;
37
- if (!el) return;
38
- userScrolledUp.current = false;
39
- el.scrollTo({
40
- top: el.scrollHeight,
41
- behavior: "smooth"
42
- });
43
- setIsAtBottom(true);
44
- }, [containerRef])
9
+ isAtBottom: atBottom,
10
+ scrollToBottom: useCallback(() => jump("smooth"), [jump])
45
11
  };
46
12
  }
47
13
  export { useAutoScroll };
@@ -0,0 +1,15 @@
1
+ /** Authenticated hosts own reconnects and session lifecycle; native streams use this lifecycle. */
2
+ export interface EventStreamSource {
3
+ addEventListener(type: string, listener: (event: MessageEvent<string>) => void): void;
4
+ close(): void;
5
+ }
6
+ export type EventStreamOpener = (url: string, onLiveChange?: (live: boolean) => void) => EventStreamSource;
7
+ export interface EventStreamHandlers {
8
+ events: Readonly<Record<string, (event: MessageEvent<string>) => void>>;
9
+ opener?: EventStreamOpener;
10
+ after?: () => number;
11
+ onStart?: () => void;
12
+ onLiveChange?: (live: boolean) => void;
13
+ }
14
+ /** Producer-owned named events; host transports keep their authenticated reconnect lifecycle. */
15
+ export declare function useEventStream(url: string | undefined, handlers: EventStreamHandlers): void;
@@ -0,0 +1,71 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "react";
3
+ // src/hooks/use-event-stream.ts
4
+ /** The sole native EventSource lifecycle, shared by buffered and producer-owned streams. */
5
+ function openNativeStream(url, handlers) {
6
+ let stopped = false, source, retry, attempt = 0;
7
+ handlers.onStart?.();
8
+ const open = () => {
9
+ handlers.onConnecting?.(attempt);
10
+ const target = new URL(typeof url === "function" ? url() : url, globalThis.location?.href), after = handlers.after?.();
11
+ if (after !== void 0 && after > 0) target.searchParams.set("after", String(after));
12
+ let connection;
13
+ const live = (value) => {
14
+ if (!stopped && (!connection || source === connection)) handlers.onLiveChange?.(value);
15
+ };
16
+ connection = handlers.opener ? handlers.opener(target.toString(), live) : new EventSource(target.toString());
17
+ source = connection;
18
+ for (const name of /* @__PURE__ */ new Set([...Object.keys(handlers.events), ...handlers.terminalEvent ? [handlers.terminalEvent] : []])) connection.addEventListener(name, (event) => {
19
+ if (stopped || source !== connection) return;
20
+ if (name === handlers.terminalEvent) {
21
+ stopped = true;
22
+ clearTimeout(retry);
23
+ connection.close();
24
+ }
25
+ handlers.events[name]?.(event);
26
+ });
27
+ if (!handlers.opener) {
28
+ const native = connection;
29
+ native.onopen = () => {
30
+ attempt = 0;
31
+ live(true);
32
+ };
33
+ native.onerror = () => {
34
+ connection.close();
35
+ if (stopped || source !== connection) return;
36
+ live(false);
37
+ const delay = handlers.retryDelay ? handlers.retryDelay(attempt++) : 1e3;
38
+ if (delay !== void 0) retry = setTimeout(open, delay);
39
+ };
40
+ }
41
+ };
42
+ open();
43
+ return { close: () => {
44
+ stopped = true;
45
+ clearTimeout(retry);
46
+ source?.close();
47
+ } };
48
+ }
49
+ /** Producer-owned named events; host transports keep their authenticated reconnect lifecycle. */
50
+ function useEventStream(url, handlers) {
51
+ const latest = useRef(handlers);
52
+ latest.current = handlers;
53
+ const names = Object.keys(handlers.events).sort().join(",");
54
+ useEffect(() => {
55
+ if (!url || !handlers.opener && typeof EventSource === "undefined") return;
56
+ const connection = openNativeStream(url, {
57
+ opener: handlers.opener,
58
+ events: Object.fromEntries(Object.keys(handlers.events).map((name) => [name, (event) => latest.current.events[name]?.(event)])),
59
+ after: () => latest.current.after?.() ?? 0,
60
+ onStart: () => latest.current.onStart?.(),
61
+ onLiveChange: (live) => latest.current.onLiveChange?.(live),
62
+ terminalEvent: "end"
63
+ });
64
+ return () => connection.close();
65
+ }, [
66
+ url,
67
+ handlers.opener,
68
+ names
69
+ ]);
70
+ }
71
+ export { openNativeStream, useEventStream };
@@ -0,0 +1,14 @@
1
+ import { RefObject } from "react";
2
+ /** Keeps the bottom through measured content growth, until the reader deliberately scrolls away. */
3
+ export declare function useFollowTail(scroller: RefObject<HTMLElement | null>, { identity, content, follow, target, thresholdPx }: {
4
+ identity?: unknown;
5
+ content?: unknown;
6
+ follow?: boolean;
7
+ target?: unknown;
8
+ thresholdPx?: number;
9
+ }): {
10
+ following: RefObject<boolean>;
11
+ unseen: boolean;
12
+ jump: (behavior?: ScrollBehavior) => void;
13
+ atBottom: boolean;
14
+ };
@@ -0,0 +1,69 @@
1
+ "use client";
2
+ import { useCallback, useLayoutEffect, useRef, useState } from "react";
3
+ // src/hooks/use-follow-tail.ts
4
+ /** Keeps the bottom through measured content growth, until the reader deliberately scrolls away. */
5
+ function useFollowTail(scroller, { identity, content, follow = true, target, thresholdPx = 40 }) {
6
+ const following = useRef(false), latest = useRef({
7
+ follow,
8
+ target,
9
+ thresholdPx
10
+ });
11
+ latest.current = {
12
+ follow,
13
+ target,
14
+ thresholdPx
15
+ };
16
+ const [unseen, setUnseen] = useState(false), [atBottom, setAtBottom] = useState(true);
17
+ const jump = useCallback((behavior = "auto") => {
18
+ const node = scroller.current;
19
+ following.current = latest.current.follow;
20
+ setUnseen(false);
21
+ setAtBottom(true);
22
+ if (node) node.scrollTo({
23
+ top: node.scrollHeight,
24
+ behavior
25
+ });
26
+ }, [scroller]);
27
+ useLayoutEffect(() => {
28
+ following.current = latest.current.follow && !latest.current.target;
29
+ setUnseen(false);
30
+ if (scroller.current && !latest.current.target) scroller.current.scrollTop = following.current ? scroller.current.scrollHeight : 0;
31
+ }, [identity]);
32
+ useLayoutEffect(() => {
33
+ if (!follow) following.current = false;
34
+ }, [follow]);
35
+ useLayoutEffect(() => {
36
+ const node = scroller.current;
37
+ if (!node) return;
38
+ const track = () => {
39
+ const near = node.scrollHeight - node.scrollTop - node.clientHeight < latest.current.thresholdPx;
40
+ following.current = latest.current.follow && near;
41
+ setAtBottom(near);
42
+ if (near) setUnseen(false);
43
+ };
44
+ node.addEventListener("scroll", track, { passive: true });
45
+ const pin = () => {
46
+ if (following.current) node.scrollTop = node.scrollHeight;
47
+ };
48
+ if (!following.current) setUnseen(true);
49
+ pin();
50
+ const observer = new ResizeObserver(pin);
51
+ observer.observe(node);
52
+ for (const child of node.children) observer.observe(child);
53
+ return () => {
54
+ observer.disconnect();
55
+ node.removeEventListener("scroll", track);
56
+ };
57
+ }, [
58
+ content,
59
+ follow,
60
+ identity
61
+ ]);
62
+ return {
63
+ following,
64
+ unseen,
65
+ jump,
66
+ atBottom
67
+ };
68
+ }
69
+ export { useFollowTail };
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import { openNativeStream } from "./use-event-stream.js";
2
3
  import * as React from "react";
3
4
  // src/hooks/use-sse-stream.ts
4
5
  /**
@@ -151,43 +152,45 @@ function useSSEStream(options) {
151
152
  }
152
153
  });
153
154
  } else {
154
- const es = new EventSource(connectUrl.toString());
155
- eventSourceRef.current = es;
156
- es.onopen = () => {
157
- setState("connected");
158
- retryCountRef.current = 0;
159
- };
160
- es.onerror = () => {
161
- const err = /* @__PURE__ */ new Error("EventSource connection error");
162
- setError(err);
163
- onError?.(err);
164
- if (autoReconnect && enabled && retryCountRef.current < maxRetries) {
165
- setState("reconnecting");
166
- es.close();
167
- const delay = reconnectDelay * 2 ** retryCountRef.current;
168
- reconnectTimeoutRef.current = setTimeout(() => {
169
- retryCountRef.current += 1;
170
- connect();
171
- }, Math.min(delay, 3e4));
172
- } else setState("error");
173
- };
174
- const types = eventTypes || ["message"];
175
- for (const type of types) es.addEventListener(type, (e) => {
155
+ const events = Object.fromEntries((eventTypes ?? ["message"]).map((type) => [type, (event) => {
156
+ let data;
176
157
  try {
177
- const data = JSON.parse(e.data);
178
- handleEvent(type, data, e.lastEventId);
158
+ data = JSON.parse(event.data);
179
159
  } catch {
180
- handleEvent(type, e.data, e.lastEventId);
160
+ data = event.data;
181
161
  }
182
- });
183
- if (!eventTypes) es.onmessage = (e) => {
184
- try {
185
- const data = JSON.parse(e.data);
186
- handleEvent("message", data, e.lastEventId);
187
- } catch {
188
- handleEvent("message", e.data, e.lastEventId);
162
+ handleEvent(type, data, event.lastEventId);
163
+ }]));
164
+ eventSourceRef.current = openNativeStream(() => {
165
+ const target = new URL(url, window.location.origin);
166
+ if (lastEventIdRef.current) target.searchParams.set("lastEventId", lastEventIdRef.current);
167
+ return target.toString();
168
+ }, {
169
+ events,
170
+ onConnecting: (attempt) => {
171
+ retryCountRef.current = attempt;
172
+ setState("connecting");
173
+ setError(null);
174
+ },
175
+ onLiveChange: (live) => {
176
+ if (live) {
177
+ setState("connected");
178
+ retryCountRef.current = 0;
179
+ return;
180
+ }
181
+ const error = /* @__PURE__ */ new Error("EventSource connection error");
182
+ setError(error);
183
+ onError?.(error);
184
+ },
185
+ retryDelay: (attempt) => {
186
+ if (!autoReconnect || !enabled || attempt >= maxRetries) {
187
+ setState("error");
188
+ return;
189
+ }
190
+ setState("reconnecting");
191
+ return Math.min(reconnectDelay * 2 ** attempt, 3e4);
189
192
  }
190
- };
193
+ });
191
194
  }
192
195
  }, [
193
196
  url,
package/dist/hooks.d.ts CHANGED
@@ -8,4 +8,6 @@ import { AuthUser, UseAuthOptions, UseAuthResult, createAuthFetcher, useApiKey,
8
8
  import { AppendUserMessageOptions, ApplySdkEventOptions, BeginAssistantMessageOptions, CompleteAssistantMessageOptions, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, UseSdkSessionOptions, UseSdkSessionReturn, useSdkSession } from "./hooks/use-sdk-session.js";
9
9
  import { RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, useRealtimeSession } from "./hooks/use-realtime-session.js";
10
10
  import { AgentStreamEvent, AutomationStreamEvent, BotStreamEvent, ConnectionState, SSEEvent, TaskStreamEvent, TerminalStreamEvent, UseSSEStreamOptions, UseSSEStreamResult, useSSEStream } from "./hooks/use-sse-stream.js";
11
- export { AgentStreamEvent, AppendUserMessageOptions, ApplySdkEventOptions, AuthUser, AutomationStreamEvent, BeginAssistantMessageOptions, BotStreamEvent, CompleteAssistantMessageOptions, ConnectionState, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, SSEEvent, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, TaskStreamEvent, TerminalStreamEvent, UseAuthOptions, UseAuthResult, UseRunGroupsOptions, UseSSEStreamOptions, UseSSEStreamResult, UseSdkSessionOptions, UseSdkSessionReturn, UseToolCallStreamReturn, createAuthFetcher, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useLiveTime, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useToolCallStream };
11
+ import { EventStreamHandlers, EventStreamOpener, EventStreamSource, useEventStream } from "./hooks/use-event-stream.js";
12
+ import { useFollowTail } from "./hooks/use-follow-tail.js";
13
+ export { AgentStreamEvent, AppendUserMessageOptions, ApplySdkEventOptions, AuthUser, AutomationStreamEvent, BeginAssistantMessageOptions, BotStreamEvent, CompleteAssistantMessageOptions, ConnectionState, type EventStreamHandlers, type EventStreamOpener, type EventStreamSource, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, SSEEvent, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, TaskStreamEvent, TerminalStreamEvent, UseAuthOptions, UseAuthResult, UseRunGroupsOptions, UseSSEStreamOptions, UseSSEStreamResult, UseSdkSessionOptions, UseSdkSessionReturn, UseToolCallStreamReturn, createAuthFetcher, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useEventStream, useFollowTail, useLiveTime, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useToolCallStream };
package/dist/hooks.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { useRunGroups } from "./hooks/use-run-groups.js";
2
2
  import { useRunCollapseState } from "./hooks/use-run-collapse-state.js";
3
+ import { useFollowTail } from "./hooks/use-follow-tail.js";
3
4
  import { useAutoScroll } from "./hooks/use-auto-scroll.js";
4
5
  import { useDropdownMenu } from "./hooks/use-dropdown-menu.js";
5
6
  import { useLiveTime } from "./hooks/use-live-time.js";
@@ -7,5 +8,6 @@ import { useToolCallStream } from "./hooks/use-tool-call-stream.js";
7
8
  import { createAuthFetcher, useApiKey, useAuth } from "./hooks/use-auth.js";
8
9
  import { RealtimeSessionRegistry, useRealtimeSession } from "./hooks/use-realtime-session.js";
9
10
  import { useSdkSession } from "./hooks/use-sdk-session.js";
11
+ import { useEventStream } from "./hooks/use-event-stream.js";
10
12
  import { useSSEStream } from "./hooks/use-sse-stream.js";
11
- export { RealtimeSessionRegistry, createAuthFetcher, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useLiveTime, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useToolCallStream };
13
+ export { RealtimeSessionRegistry, createAuthFetcher, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useEventStream, useFollowTail, useLiveTime, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useToolCallStream };
package/dist/index.d.ts CHANGED
@@ -49,6 +49,8 @@ import { AppendUserMessageOptions, ApplySdkEventOptions, BeginAssistantMessageOp
49
49
  import { ActiveProjectActivity, ActiveSessionActivityOptions, ActiveSessionConnectionOptions, ActiveSessionConnectionState, ActiveSessionReconnectState, ActiveSessionRecord, ActiveSessionStatus, ActiveSessionTransportMode, ActiveSessionsState, RegisterActiveSessionOptions, SessionProjectKey, activeSessionsAtom, bumpActiveSessionActivity, getActiveSession, getAllActiveSessions, getAllProjectActivity, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getTotalRunningSessionCount, hasBackgroundRunningSessions, registerActiveSession, resetActiveSessions, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, unregisterActiveSession, updateActiveSessionMeta, useActiveSession, useActiveSessions, useActiveSessionsState, useHasBackgroundRunningSessions, useNavbarSessions, useProjectActivity, useProjectSessions, useSessionsByActivity, useTotalRunningSessions } from "./stores/active-sessions-store.js";
50
50
  import { RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, useRealtimeSession } from "./hooks/use-realtime-session.js";
51
51
  import { AgentStreamEvent, AutomationStreamEvent, BotStreamEvent, ConnectionState, SSEEvent, TaskStreamEvent, TerminalStreamEvent, UseSSEStreamOptions, UseSSEStreamResult, useSSEStream } from "./hooks/use-sse-stream.js";
52
+ import { EventStreamHandlers, EventStreamOpener, EventStreamSource, useEventStream } from "./hooks/use-event-stream.js";
53
+ import { useFollowTail } from "./hooks/use-follow-tail.js";
52
54
  import "./hooks.js";
53
55
  import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CardTitleProps } from "./primitives/card.js";
54
56
  import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "./primitives/dialog.js";
@@ -145,4 +147,4 @@ import { QuestionPreview, QuestionPreviewProps } from "./tool-previews/question-
145
147
  import { DiffPreview, DiffPreviewProps } from "./tool-previews/diff-preview.js";
146
148
  import { RedactedDocSegment, RedactedDocument, RedactedDocumentData, RedactedDocumentProps, RevealResult } from "./redaction/redacted-document.js";
147
149
  import "./redaction.js";
148
- export { ActiveProjectActivity, ActiveSessionActivityOptions, ActiveSessionConnectionOptions, ActiveSessionConnectionState, ActiveSessionReconnectState, ActiveSessionRecord, ActiveSessionStatus, ActiveSessionTransportMode, ActiveSessionsState, AgentBranding, AgentStreamEvent, AgentTimeline, type AgentTimelineArtifactItem, AgentTimelineColumn, type AgentTimelineCustomItem, type AgentTimelineFoldItem, type AgentTimelineItem, type AgentTimelineMessageItem, type AgentTimelineProps, type AgentTimelineStatusItem, AgentTimelineStep, type AgentTimelineStepKind, type AgentTimelineStepProps, type AgentTimelineTone, type AgentTimelineToolGroupItem, type AgentTimelineToolItem, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, type AlertTone, ApiSchema, type ApiSchemaProps, AppendUserMessageOptions, ApplySdkEventOptions, ApprovalCard, type ApprovalCardProps, type ApprovalDiffFile, ApprovalDiffSummary, type ApprovalPlanStep, ArtifactPane, type ArtifactPaneProps, AuthHeader, type AuthHeaderProps, AuthUser, AutomationStreamEvent, Avatar, AvatarFallback, AvatarImage, type AvatarProps, Badge, type BadgeProps, BeginAssistantMessageOptions, BotStreamEvent, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Calendar, type CalendarDate, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, type ChartReferenceLine, ChatAuthor, ChatContainer, type ChatContainerProps, ChatMessage, type ChatMessageProps, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, type CommandPreviewProps, CompleteAssistantMessageOptions, ComplianceBadge, type ComplianceBadgeProps, type ComplianceProduct, type ComplianceStatus, ConnectionState, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, type CopyButtonProps, CustomToolRenderer, DatePicker, type DatePickerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, type DiffPreviewProps, DisplayVariant, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, ExpandedToolDetail, type ExpandedToolDetailProps, type FeedSegment, Field, type FieldProps, FileArtifactPane, type FileArtifactPaneProps, FileCard, type FileCardProps, type FileDetails, type FileFormat, FileList, type FileListItem, type FileListProps, type FileNode, FilePreview, type FilePreviewKind, type FilePreviewProps, type FileTabData, FileTabs, type FileTabsProps, FileTree, type FileTreeProps, type FileTreeVisibilityOptions, type FileUploader, FilterField, type FilterFieldProps, FinalTextPart, GitHubLoginButton, type GitHubLoginButtonProps, GlobResultsPreview, type GlobResultsPreviewProps, GrepResultsPreview, type GrepResultsPreviewProps, GroupedMessage, Heading, type HeadingProps, type HeadingVariant, HelpText, type HelpTextProps, HoverCard, HoverCardContent, HoverCardDescription, HoverCardHeader, HoverCardTitle, HoverCardTrigger, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, InlineThinkingItem, type InlineThinkingItemProps, InlineToolItem, type InlineToolItemProps, Input, type InputProps, Label, LiveDuration, LoginLayout, type LoginLayoutProps, Logo, type LogoProps, Markdown, type MarkdownLinkChip, type MarkdownLinkChipResolver, type MarkdownProps, MessageAuthor, type MessageAuthorProps, MessageList, type MessageListProps, type MessageRole, MessageRun, MessageUser, Metric, MetricChart, type MetricChartProps, type MetricProps, MetricStrip, type MetricStripProps, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, type OpenUIAction, type OpenUIActionsNode, OpenUIArtifactRenderer, type OpenUIArtifactRendererProps, type OpenUIBadgeNode, type OpenUICardNode, type OpenUICodeNode, type OpenUIComponentNode, type OpenUIGridNode, type OpenUIHeadingNode, type OpenUIKeyValueNode, type OpenUIMarkdownNode, type OpenUIPrimitive, type OpenUISchemaIssue, type OpenUISchemaValidation, type OpenUISeparatorNode, type OpenUIStackNode, type OpenUIStatNode, type OpenUITableNode, type OpenUITextNode, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Pagination, type PaginationProps, PdfViewer, type PdfViewerConfig, type PdfViewerProps, type PeriodDelta, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, type QuestionPreviewProps, RadioGroup, RadioGroupItem, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, ReasoningPart, type RedactedDocSegment, RedactedDocument, type RedactedDocumentData, type RedactedDocumentProps, RegisterActiveSessionOptions, type RevealResult, RichFileTree, type RichFileTreeGitEntry, type RichFileTreeGitStatus, type RichFileTreeProps, type RichFileTreeThemeVars, Run, RunGroup, type RunGroupProps, type RunPhase, RunPhaseList, RunRowShell, type RunRowShellProps, type RunRowStatus, RunRowStatusDot, RunStats, type RunStatus, RunStatusIcon, type RunStatusIconProps, SSEEvent, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeriesExtent, type SeriesPoint, type SeriesPointInput, type SeriesScale, SeriesSparkline, type SeriesSparklineProps, SessionMessage, SessionPart, SessionProjectKey, type SessionUser, SettingsList, type SettingsListAction, type SettingsListColumns, SettingsListItem, type SettingsListItemProps, type SettingsListMenuItem, type SettingsListProps, SettingsNote, type SettingsNoteProps, SettingsRow, type SettingsRowLayout, type SettingsRowProps, SettingsSection, type SettingsSectionProps, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, type Speaker, SpeakerHeader, type SpeakerHeaderProps, type SpeakerPart, type SpeakerSection, type SpeakerState, StatCard, type StatCardProps, type StatusMeta, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, TRANSCRIPT_TEXT, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TaskList, type TaskListItem, type TaskListProps, type TaskStatus, TaskStreamEvent, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TerminalStreamEvent, TextPart, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, ThinkingIndicator, type ThinkingIndicatorProps, TimeSelect, type TimeSelectProps, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, type ToolCallData, ToolCallFeed, type ToolCallFeedProps, type ToolCallStatus, type ToolCallType, ToolCategory, ToolDisplayMetadata, ToolPart, ToolState, ToolStatus, ToolTime, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, UseAuthOptions, UseAuthResult, UseRunGroupsOptions, UseSSEStreamOptions, UseSSEStreamResult, UseSdkSessionOptions, UseSdkSessionReturn, UseToolCallStreamReturn, UserMenu, type UserMenuProps, UserMessage, type UserMessageProps, WebSearchPreview, type WebSearchPreviewProps, Wizard, type WizardProps, WizardStep, type WizardStepProps, WriteFilePreview, type WriteFilePreviewProps, activeSessionsAtom, addMessage, addParts, agentTimelineStepKind, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, codeBlockGrammar, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fenceInfo, fieldMessageId, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, foldSummary, formatBytes, formatCalendarDate, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, localToday, messagesAtom, normalizeSeries, numericColumns, parseCalendarDate, parseCsv, parseToolEvent, partMapAtom, partitionBySpeaker, periodDelta, plainTextFromMarkdown, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, runStatus, seriesAreaPath, seriesExtent, seriesLinePath, seriesMean, seriesScale, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, showsTaskPriority, statusMeta, tableCsv, taskProgress, taskStatus, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
150
+ export { ActiveProjectActivity, ActiveSessionActivityOptions, ActiveSessionConnectionOptions, ActiveSessionConnectionState, ActiveSessionReconnectState, ActiveSessionRecord, ActiveSessionStatus, ActiveSessionTransportMode, ActiveSessionsState, AgentBranding, AgentStreamEvent, AgentTimeline, type AgentTimelineArtifactItem, AgentTimelineColumn, type AgentTimelineCustomItem, type AgentTimelineFoldItem, type AgentTimelineItem, type AgentTimelineMessageItem, type AgentTimelineProps, type AgentTimelineStatusItem, AgentTimelineStep, type AgentTimelineStepKind, type AgentTimelineStepProps, type AgentTimelineTone, type AgentTimelineToolGroupItem, type AgentTimelineToolItem, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, type AlertTone, ApiSchema, type ApiSchemaProps, AppendUserMessageOptions, ApplySdkEventOptions, ApprovalCard, type ApprovalCardProps, type ApprovalDiffFile, ApprovalDiffSummary, type ApprovalPlanStep, ArtifactPane, type ArtifactPaneProps, AuthHeader, type AuthHeaderProps, AuthUser, AutomationStreamEvent, Avatar, AvatarFallback, AvatarImage, type AvatarProps, Badge, type BadgeProps, BeginAssistantMessageOptions, BotStreamEvent, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Calendar, type CalendarDate, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, type ChartReferenceLine, ChatAuthor, ChatContainer, type ChatContainerProps, ChatMessage, type ChatMessageProps, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, type CommandPreviewProps, CompleteAssistantMessageOptions, ComplianceBadge, type ComplianceBadgeProps, type ComplianceProduct, type ComplianceStatus, ConnectionState, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, type CopyButtonProps, CustomToolRenderer, DatePicker, type DatePickerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, type DiffPreviewProps, DisplayVariant, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, type EventStreamHandlers, type EventStreamOpener, type EventStreamSource, ExpandedToolDetail, type ExpandedToolDetailProps, type FeedSegment, Field, type FieldProps, FileArtifactPane, type FileArtifactPaneProps, FileCard, type FileCardProps, type FileDetails, type FileFormat, FileList, type FileListItem, type FileListProps, type FileNode, FilePreview, type FilePreviewKind, type FilePreviewProps, type FileTabData, FileTabs, type FileTabsProps, FileTree, type FileTreeProps, type FileTreeVisibilityOptions, type FileUploader, FilterField, type FilterFieldProps, FinalTextPart, GitHubLoginButton, type GitHubLoginButtonProps, GlobResultsPreview, type GlobResultsPreviewProps, GrepResultsPreview, type GrepResultsPreviewProps, GroupedMessage, Heading, type HeadingProps, type HeadingVariant, HelpText, type HelpTextProps, HoverCard, HoverCardContent, HoverCardDescription, HoverCardHeader, HoverCardTitle, HoverCardTrigger, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, InlineThinkingItem, type InlineThinkingItemProps, InlineToolItem, type InlineToolItemProps, Input, type InputProps, Label, LiveDuration, LoginLayout, type LoginLayoutProps, Logo, type LogoProps, Markdown, type MarkdownLinkChip, type MarkdownLinkChipResolver, type MarkdownProps, MessageAuthor, type MessageAuthorProps, MessageList, type MessageListProps, type MessageRole, MessageRun, MessageUser, Metric, MetricChart, type MetricChartProps, type MetricProps, MetricStrip, type MetricStripProps, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, type OpenUIAction, type OpenUIActionsNode, OpenUIArtifactRenderer, type OpenUIArtifactRendererProps, type OpenUIBadgeNode, type OpenUICardNode, type OpenUICodeNode, type OpenUIComponentNode, type OpenUIGridNode, type OpenUIHeadingNode, type OpenUIKeyValueNode, type OpenUIMarkdownNode, type OpenUIPrimitive, type OpenUISchemaIssue, type OpenUISchemaValidation, type OpenUISeparatorNode, type OpenUIStackNode, type OpenUIStatNode, type OpenUITableNode, type OpenUITextNode, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Pagination, type PaginationProps, PdfViewer, type PdfViewerConfig, type PdfViewerProps, type PeriodDelta, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, type QuestionPreviewProps, RadioGroup, RadioGroupItem, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, ReasoningPart, type RedactedDocSegment, RedactedDocument, type RedactedDocumentData, type RedactedDocumentProps, RegisterActiveSessionOptions, type RevealResult, RichFileTree, type RichFileTreeGitEntry, type RichFileTreeGitStatus, type RichFileTreeProps, type RichFileTreeThemeVars, Run, RunGroup, type RunGroupProps, type RunPhase, RunPhaseList, RunRowShell, type RunRowShellProps, type RunRowStatus, RunRowStatusDot, RunStats, type RunStatus, RunStatusIcon, type RunStatusIconProps, SSEEvent, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeriesExtent, type SeriesPoint, type SeriesPointInput, type SeriesScale, SeriesSparkline, type SeriesSparklineProps, SessionMessage, SessionPart, SessionProjectKey, type SessionUser, SettingsList, type SettingsListAction, type SettingsListColumns, SettingsListItem, type SettingsListItemProps, type SettingsListMenuItem, type SettingsListProps, SettingsNote, type SettingsNoteProps, SettingsRow, type SettingsRowLayout, type SettingsRowProps, SettingsSection, type SettingsSectionProps, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, type Speaker, SpeakerHeader, type SpeakerHeaderProps, type SpeakerPart, type SpeakerSection, type SpeakerState, StatCard, type StatCardProps, type StatusMeta, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, TRANSCRIPT_TEXT, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TaskList, type TaskListItem, type TaskListProps, type TaskStatus, TaskStreamEvent, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TerminalStreamEvent, TextPart, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, ThinkingIndicator, type ThinkingIndicatorProps, TimeSelect, type TimeSelectProps, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, type ToolCallData, ToolCallFeed, type ToolCallFeedProps, type ToolCallStatus, type ToolCallType, ToolCategory, ToolDisplayMetadata, ToolPart, ToolState, ToolStatus, ToolTime, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, UseAuthOptions, UseAuthResult, UseRunGroupsOptions, UseSSEStreamOptions, UseSSEStreamResult, UseSdkSessionOptions, UseSdkSessionReturn, UseToolCallStreamReturn, UserMenu, type UserMenuProps, UserMessage, type UserMessageProps, WebSearchPreview, type WebSearchPreviewProps, Wizard, type WizardProps, WizardStep, type WizardStepProps, WriteFilePreview, type WriteFilePreviewProps, activeSessionsAtom, addMessage, addParts, agentTimelineStepKind, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, codeBlockGrammar, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fenceInfo, fieldMessageId, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, foldSummary, formatBytes, formatCalendarDate, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, localToday, messagesAtom, normalizeSeries, numericColumns, parseCalendarDate, parseCsv, parseToolEvent, partMapAtom, partitionBySpeaker, periodDelta, plainTextFromMarkdown, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, runStatus, seriesAreaPath, seriesExtent, seriesLinePath, seriesMean, seriesScale, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, showsTaskPriority, statusMeta, tableCsv, taskProgress, taskStatus, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useEventStream, useFollowTail, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
package/dist/index.js CHANGED
@@ -69,6 +69,7 @@ import { Logo, TangleKnot } from "./primitives.js";
69
69
  import { TOOL_CATEGORY_ICONS, getToolCategory, getToolDisplayMetadata, getToolErrorText } from "./utils/tool-display.js";
70
70
  import { useRunGroups } from "./hooks/use-run-groups.js";
71
71
  import { useRunCollapseState } from "./hooks/use-run-collapse-state.js";
72
+ import { useFollowTail } from "./hooks/use-follow-tail.js";
72
73
  import { useAutoScroll } from "./hooks/use-auto-scroll.js";
73
74
  import { formatBytes, formatDuration, formatUptime, plainTextFromMarkdown, truncateText } from "./utils/format.js";
74
75
  import { CommandPreview } from "./tool-previews/command-preview.js";
@@ -124,9 +125,10 @@ import { createAuthFetcher, useApiKey, useAuth } from "./hooks/use-auth.js";
124
125
  import { activeSessionsAtom, bumpActiveSessionActivity, getActiveSession, getAllActiveSessions, getAllProjectActivity, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getTotalRunningSessionCount, hasBackgroundRunningSessions, registerActiveSession, resetActiveSessions, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, unregisterActiveSession, updateActiveSessionMeta, useActiveSession, useActiveSessions, useActiveSessionsState, useHasBackgroundRunningSessions, useNavbarSessions, useProjectActivity, useProjectSessions, useSessionsByActivity, useTotalRunningSessions } from "./stores/active-sessions-store.js";
125
126
  import { RealtimeSessionRegistry, useRealtimeSession } from "./hooks/use-realtime-session.js";
126
127
  import { useSdkSession } from "./hooks/use-sdk-session.js";
128
+ import { useEventStream } from "./hooks/use-event-stream.js";
127
129
  import { useSSEStream } from "./hooks/use-sse-stream.js";
128
130
  import { addMessage, addParts, clearChat, isStreamingAtom, messagesAtom, partMapAtom, updatePart } from "./stores/chat-store.js";
129
131
  import { copyText } from "./utils/copy-text.js";
130
132
  import { timeAgo } from "./utils/time-ago.js";
131
133
  import { RedactedDocument } from "./redaction/redacted-document.js";
132
- export { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ApiSchema, ApprovalCard, ApprovalDiffSummary, ArtifactPane, AuthHeader, Avatar, AvatarFallback, AvatarImage, Badge, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ChatContainer, ChatMessage, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, ComplianceBadge, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, ExpandedToolDetail, Field, FileArtifactPane, FileCard, FileList, FilePreview, FileTabs, FileTree, FilterField, GitHubLoginButton, GlobResultsPreview, GrepResultsPreview, Heading, HelpText, HoverCard, HoverCardContent, HoverCardDescription, HoverCardHeader, HoverCardTitle, HoverCardTrigger, IconTile, InlineCode, InlineThinkingItem, InlineToolItem, Input, Label, LiveDuration, LoginLayout, Logo, Markdown, MessageAuthor, MessageList, Metric, MetricChart, MetricStrip, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, OpenUIArtifactRenderer, PageHeader, PageShell, Pagination, PdfViewer, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, RadioGroup, RadioGroupItem, RealtimeSessionRegistry, RedactedDocument, RichFileTree, RunGroup, RunPhaseList, RunRowShell, RunRowStatusDot, RunStatusIcon, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SeriesSparkline, SettingsList, SettingsListItem, SettingsNote, SettingsRow, SettingsSection, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, SpeakerHeader, StatCard, StatusPill, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, TRANSCRIPT_TEXT, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TaskList, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ThinkingIndicator, TimeSelect, ToastContainer, ToastProvider, ToolCallFeed, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, UserMenu, UserMessage, WebSearchPreview, Wizard, WizardStep, WriteFilePreview, activeSessionsAtom, addMessage, addParts, agentTimelineStepKind, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, codeBlockGrammar, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fenceInfo, fieldMessageId, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, foldSummary, formatBytes, formatCalendarDate, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, localToday, messagesAtom, normalizeSeries, numericColumns, parseCalendarDate, parseCsv, parseToolEvent, partMapAtom, partitionBySpeaker, periodDelta, plainTextFromMarkdown, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, runStatus, seriesAreaPath, seriesExtent, seriesLinePath, seriesMean, seriesScale, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, showsTaskPriority, statusMeta, tableCsv, taskProgress, taskStatus, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
134
+ export { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ApiSchema, ApprovalCard, ApprovalDiffSummary, ArtifactPane, AuthHeader, Avatar, AvatarFallback, AvatarImage, Badge, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ChatContainer, ChatMessage, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, ComplianceBadge, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, ExpandedToolDetail, Field, FileArtifactPane, FileCard, FileList, FilePreview, FileTabs, FileTree, FilterField, GitHubLoginButton, GlobResultsPreview, GrepResultsPreview, Heading, HelpText, HoverCard, HoverCardContent, HoverCardDescription, HoverCardHeader, HoverCardTitle, HoverCardTrigger, IconTile, InlineCode, InlineThinkingItem, InlineToolItem, Input, Label, LiveDuration, LoginLayout, Logo, Markdown, MessageAuthor, MessageList, Metric, MetricChart, MetricStrip, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, OpenUIArtifactRenderer, PageHeader, PageShell, Pagination, PdfViewer, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, RadioGroup, RadioGroupItem, RealtimeSessionRegistry, RedactedDocument, RichFileTree, RunGroup, RunPhaseList, RunRowShell, RunRowStatusDot, RunStatusIcon, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SeriesSparkline, SettingsList, SettingsListItem, SettingsNote, SettingsRow, SettingsSection, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, SpeakerHeader, StatCard, StatusPill, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, TRANSCRIPT_TEXT, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TaskList, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ThinkingIndicator, TimeSelect, ToastContainer, ToastProvider, ToolCallFeed, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, UserMenu, UserMessage, WebSearchPreview, Wizard, WizardStep, WriteFilePreview, activeSessionsAtom, addMessage, addParts, agentTimelineStepKind, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, codeBlockGrammar, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fenceInfo, fieldMessageId, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, foldSummary, formatBytes, formatCalendarDate, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, localToday, messagesAtom, normalizeSeries, numericColumns, parseCalendarDate, parseCsv, parseToolEvent, partMapAtom, partitionBySpeaker, periodDelta, plainTextFromMarkdown, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, runStatus, seriesAreaPath, seriesExtent, seriesLinePath, seriesMean, seriesScale, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, showsTaskPriority, statusMeta, tableCsv, taskProgress, taskStatus, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useEventStream, useFollowTail, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
@@ -46,6 +46,7 @@ const META = {
46
46
  };
47
47
  const ALIASES = {
48
48
  active: "running",
49
+ working: "running",
49
50
  in_progress: "running",
50
51
  "in-progress": "running",
51
52
  streaming: "running",
@@ -56,6 +57,7 @@ const ALIASES = {
56
57
  needs_you: "needs-you",
57
58
  needs_input: "needs-you",
58
59
  awaiting_input: "needs-you",
60
+ winner: "done",
59
61
  completed: "done",
60
62
  complete: "done",
61
63
  success: "done",
@@ -63,6 +65,9 @@ const ALIASES = {
63
65
  shipped: "done",
64
66
  error: "failed",
65
67
  errored: "failed",
68
+ "driver-failed": "failed",
69
+ down: "stopped",
70
+ "no-winner": "stopped",
66
71
  cancelled: "stopped",
67
72
  canceled: "stopped",
68
73
  aborted: "stopped"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.51.2",
3
+ "version": "11.51.4",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -167,7 +167,7 @@
167
167
  "peerDependencies": {
168
168
  "@hocuspocus/provider": ">=2.15.0 <5.0.0",
169
169
  "@nanostores/react": ">=0.8.4 <3.0.0",
170
- "@tangle-network/brand": "^1.31.2",
170
+ "@tangle-network/brand": "^1.31.3",
171
171
  "@tiptap/core": ">=3.20.4 <4.0.0",
172
172
  "@tiptap/extension-collaboration": ">=3.20.4 <4.0.0",
173
173
  "@tiptap/extension-collaboration-caret": ">=3.7.0 <4.0.0",
@@ -8,3 +8,6 @@ export * from "./use-auth";
8
8
  export * from "./use-realtime-session";
9
9
  export * from "./use-sdk-session";
10
10
  export * from "./use-sse-stream";
11
+
12
+ export { useEventStream, type EventStreamHandlers, type EventStreamSource, type EventStreamOpener } from "./use-event-stream";
13
+ export { useFollowTail } from "./use-follow-tail";
@@ -1,59 +1,10 @@
1
- import {
2
- useCallback,
3
- useEffect,
4
- useRef,
5
- useState,
6
- type RefObject,
7
- } from "react";
8
-
9
- const BOTTOM_THRESHOLD = 40;
10
-
11
- /**
12
- * Scroll-to-bottom behaviour: sticks to the bottom while streaming,
13
- * pauses when user scrolls up, resumes when user scrolls back down.
14
- */
15
- export function useAutoScroll(
16
- containerRef: RefObject<HTMLElement | null>,
17
- deps: unknown[] = [],
18
- ) {
19
- const [isAtBottom, setIsAtBottom] = useState(true);
20
- const userScrolledUp = useRef(false);
21
-
22
- const checkBottom = useCallback(() => {
23
- const el = containerRef.current;
24
- if (!el) return true;
25
- return el.scrollHeight - el.scrollTop - el.clientHeight < BOTTOM_THRESHOLD;
26
- }, [containerRef]);
27
-
28
- useEffect(() => {
29
- const el = containerRef.current;
30
- if (!el) return;
31
-
32
- const onScroll = () => {
33
- const atBottom = checkBottom();
34
- setIsAtBottom(atBottom);
35
- userScrolledUp.current = !atBottom;
36
- };
37
-
38
- el.addEventListener("scroll", onScroll, { passive: true });
39
- return () => el.removeEventListener("scroll", onScroll);
40
- }, [containerRef, checkBottom]);
41
-
42
- useEffect(() => {
43
- if (userScrolledUp.current) return;
44
- const el = containerRef.current;
45
- if (!el) return;
46
- el.scrollTop = el.scrollHeight;
47
- // eslint-disable-next-line react-hooks/exhaustive-deps
48
- }, deps);
49
-
50
- const scrollToBottom = useCallback(() => {
51
- const el = containerRef.current;
52
- if (!el) return;
53
- userScrolledUp.current = false;
54
- el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
55
- setIsAtBottom(true);
56
- }, [containerRef]);
57
-
58
- return { isAtBottom, scrollToBottom };
1
+ import { useCallback, useMemo, type RefObject } from 'react'
2
+ import { useFollowTail } from './use-follow-tail'
3
+
4
+ /** Existing chat contract, backed by the shared measured-content following lifecycle. */
5
+ export function useAutoScroll(containerRef: RefObject<HTMLElement | null>, deps: unknown[] = []) {
6
+ const content = useMemo(() => ({}), deps)
7
+ const { atBottom, jump } = useFollowTail(containerRef, { content })
8
+ const scrollToBottom = useCallback(() => jump('smooth'), [jump])
9
+ return { isAtBottom: atBottom, scrollToBottom }
59
10
  }
@@ -0,0 +1,71 @@
1
+ "use client";
2
+ import { useEffect, useRef } from 'react'
3
+
4
+ /** Authenticated hosts own reconnects and session lifecycle; native streams use this lifecycle. */
5
+ export interface EventStreamSource {
6
+ addEventListener(type: string, listener: (event: MessageEvent<string>) => void): void
7
+ close(): void
8
+ }
9
+ export type EventStreamOpener = (url: string, onLiveChange?: (live: boolean) => void) => EventStreamSource
10
+ export interface EventStreamHandlers {
11
+ events: Readonly<Record<string, (event: MessageEvent<string>) => void>>
12
+ opener?: EventStreamOpener
13
+ after?: () => number
14
+ onStart?: () => void
15
+ onLiveChange?: (live: boolean) => void
16
+ }
17
+
18
+ /** Internal policy preserves the buffered hook's existing bounded exponential retries. */
19
+ type NativePolicy = EventStreamHandlers & { terminalEvent?: string; onConnecting?: (attempt: number) => void; retryDelay?: (attempt: number) => number | undefined }
20
+
21
+ /** The sole native EventSource lifecycle, shared by buffered and producer-owned streams. */
22
+ export function openNativeStream(url: string | (() => string), handlers: NativePolicy): { close(): void } {
23
+ let stopped = false, source: EventStreamSource | undefined, retry: ReturnType<typeof setTimeout> | undefined, attempt = 0
24
+ handlers.onStart?.()
25
+ const open = () => {
26
+ handlers.onConnecting?.(attempt)
27
+ const target = new URL(typeof url === 'function' ? url() : url, globalThis.location?.href), after = handlers.after?.()
28
+ if (after !== undefined && after > 0) target.searchParams.set('after', String(after))
29
+ let connection: EventStreamSource
30
+ const live = (value: boolean) => { if (!stopped && (!connection || source === connection)) handlers.onLiveChange?.(value) }
31
+ connection = handlers.opener ? handlers.opener(target.toString(), live) : new EventSource(target.toString())
32
+ source = connection
33
+ for (const name of new Set([...Object.keys(handlers.events), ...(handlers.terminalEvent ? [handlers.terminalEvent] : [])])) connection.addEventListener(name, event => {
34
+ if (stopped || source !== connection) return
35
+ if (name === handlers.terminalEvent) { stopped = true; clearTimeout(retry); connection.close() }
36
+ handlers.events[name]?.(event)
37
+ })
38
+ if (!handlers.opener) {
39
+ const native = connection as EventSource
40
+ native.onopen = () => { attempt = 0; live(true) }
41
+ native.onerror = () => {
42
+ connection.close()
43
+ if (stopped || source !== connection) return
44
+ live(false)
45
+ const delay = handlers.retryDelay ? handlers.retryDelay(attempt++) : 1_000
46
+ if (delay !== undefined) retry = setTimeout(open, delay)
47
+ }
48
+ }
49
+ }
50
+ open()
51
+ return { close: () => { stopped = true; clearTimeout(retry); source?.close() } }
52
+ }
53
+
54
+ /** Producer-owned named events; host transports keep their authenticated reconnect lifecycle. */
55
+ export function useEventStream(url: string | undefined, handlers: EventStreamHandlers): void {
56
+ const latest = useRef(handlers)
57
+ latest.current = handlers
58
+ const names = Object.keys(handlers.events).sort().join(',')
59
+ useEffect(() => {
60
+ if (!url || (!handlers.opener && typeof EventSource === 'undefined')) return
61
+ const current: EventStreamHandlers = {
62
+ opener: handlers.opener,
63
+ events: Object.fromEntries(Object.keys(handlers.events).map(name => [name, (event: MessageEvent<string>) => latest.current.events[name]?.(event)])),
64
+ after: () => latest.current.after?.() ?? 0,
65
+ onStart: () => latest.current.onStart?.(),
66
+ onLiveChange: live => latest.current.onLiveChange?.(live),
67
+ }
68
+ const connection = openNativeStream(url, { ...current, terminalEvent: 'end' })
69
+ return () => connection.close()
70
+ }, [url, handlers.opener, names])
71
+ }
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { type RefObject, useCallback, useLayoutEffect, useRef, useState } from 'react'
3
+
4
+ /** Keeps the bottom through measured content growth, until the reader deliberately scrolls away. */
5
+ export function useFollowTail(scroller: RefObject<HTMLElement | null>, { identity, content, follow = true, target, thresholdPx = 40 }: {
6
+ identity?: unknown; content?: unknown; follow?: boolean; target?: unknown; thresholdPx?: number
7
+ }) {
8
+ const following = useRef(false), latest = useRef({ follow, target, thresholdPx })
9
+ latest.current = { follow, target, thresholdPx }
10
+ const [unseen, setUnseen] = useState(false), [atBottom, setAtBottom] = useState(true)
11
+ const jump = useCallback((behavior: ScrollBehavior = 'auto') => { const node = scroller.current; following.current = latest.current.follow; setUnseen(false); setAtBottom(true); if (node) node.scrollTo({ top: node.scrollHeight, behavior }) }, [scroller])
12
+ useLayoutEffect(() => {
13
+ following.current = latest.current.follow && !latest.current.target
14
+ setUnseen(false)
15
+ if (scroller.current && !latest.current.target) scroller.current.scrollTop = following.current ? scroller.current.scrollHeight : 0
16
+ }, [identity])
17
+ useLayoutEffect(() => { if (!follow) following.current = false }, [follow])
18
+ useLayoutEffect(() => {
19
+ const node = scroller.current
20
+ if (!node) return
21
+ const track = () => { const near = node.scrollHeight - node.scrollTop - node.clientHeight < latest.current.thresholdPx; following.current = latest.current.follow && near; setAtBottom(near); if (near) setUnseen(false) }
22
+ node.addEventListener('scroll', track, { passive: true })
23
+ const pin = () => { if (following.current) node.scrollTop = node.scrollHeight }
24
+ if (!following.current) setUnseen(true)
25
+ pin()
26
+ const observer = new ResizeObserver(pin)
27
+ observer.observe(node)
28
+ for (const child of node.children) observer.observe(child)
29
+ return () => { observer.disconnect(); node.removeEventListener('scroll', track) }
30
+ }, [content, follow, identity])
31
+ return { following, unseen, jump, atBottom }
32
+ }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
+ import { openNativeStream } from "./use-event-stream";
4
5
 
5
6
  /**
6
7
  * SSE Event data structure
@@ -121,7 +122,7 @@ export function useSSEStream<T = unknown>(
121
122
  const [timeSinceLastEvent, setTimeSinceLastEvent] = React.useState(0);
122
123
 
123
124
  const retryCountRef = React.useRef(0);
124
- const eventSourceRef = React.useRef<EventSource | null>(null);
125
+ const eventSourceRef = React.useRef<{ close(): void } | null>(null);
125
126
  const abortControllerRef = React.useRef<AbortController | null>(null);
126
127
  const reconnectTimeoutRef = React.useRef<ReturnType<
127
128
  typeof setTimeout
@@ -330,60 +331,28 @@ export function useSSEStream<T = unknown>(
330
331
  }
331
332
  });
332
333
  } else {
333
- // Use native EventSource for simple cases
334
- const es = new EventSource(connectUrl.toString());
335
- eventSourceRef.current = es;
336
-
337
- es.onopen = () => {
338
- setState("connected");
339
- retryCountRef.current = 0;
340
- };
341
-
342
- es.onerror = () => {
343
- const err = new Error("EventSource connection error");
344
- setError(err);
345
- onError?.(err);
346
-
347
- if (autoReconnect && enabled && retryCountRef.current < maxRetries) {
348
- setState("reconnecting");
349
- es.close();
350
- const delay = reconnectDelay * 2 ** retryCountRef.current;
351
- reconnectTimeoutRef.current = setTimeout(
352
- () => {
353
- retryCountRef.current += 1;
354
- connect();
355
- },
356
- Math.min(delay, 30000),
357
- );
358
- } else {
359
- setState("error");
360
- }
361
- };
362
-
363
- // Listen for specific event types or default 'message'
364
- const types = eventTypes || ["message"];
365
- for (const type of types) {
366
- es.addEventListener(type, (e: MessageEvent) => {
367
- try {
368
- const data = JSON.parse(e.data) as T;
369
- handleEvent(type, data, e.lastEventId);
370
- } catch {
371
- handleEvent(type, e.data as T, e.lastEventId);
372
- }
373
- });
374
- }
375
-
376
- // Also listen for unnamed events
377
- if (!eventTypes) {
378
- es.onmessage = (e: MessageEvent) => {
379
- try {
380
- const data = JSON.parse(e.data) as T;
381
- handleEvent("message", data, e.lastEventId);
382
- } catch {
383
- handleEvent("message", e.data as T, e.lastEventId);
384
- }
385
- };
386
- }
334
+ const events = Object.fromEntries((eventTypes ?? ["message"]).map(type => [type, (event: MessageEvent<string>) => {
335
+ let data: T
336
+ try { data = JSON.parse(event.data) as T } catch { data = event.data as T }
337
+ handleEvent(type, data, event.lastEventId)
338
+ }]))
339
+ eventSourceRef.current = openNativeStream(() => {
340
+ const target = new URL(url, window.location.origin)
341
+ if (lastEventIdRef.current) target.searchParams.set("lastEventId", lastEventIdRef.current)
342
+ return target.toString()
343
+ }, {
344
+ events,
345
+ onConnecting: attempt => { retryCountRef.current = attempt; setState("connecting"); setError(null) },
346
+ onLiveChange: live => {
347
+ if (live) { setState("connected"); retryCountRef.current = 0; return }
348
+ const error = new Error("EventSource connection error"); setError(error); onError?.(error)
349
+ },
350
+ retryDelay: attempt => {
351
+ if (!autoReconnect || !enabled || attempt >= maxRetries) { setState("error"); return undefined }
352
+ setState("reconnecting")
353
+ return Math.min(reconnectDelay * 2 ** attempt, 30000)
354
+ },
355
+ })
387
356
  }
388
357
  }, [
389
358
  url,
@@ -42,12 +42,12 @@ const META: Record<RunStatus, StatusMeta> = {
42
42
 
43
43
  // The vocabularies ui and its products already speak, so each maps here once.
44
44
  const ALIASES: Record<string, RunStatus> = {
45
- active: "running", in_progress: "running", "in-progress": "running", streaming: "running",
45
+ active: "running", working: "running", in_progress: "running", "in-progress": "running", streaming: "running",
46
46
  pending: "waiting", queued: "waiting", blocked: "waiting",
47
47
  "attention-needed": "needs-you", needs_you: "needs-you", needs_input: "needs-you", awaiting_input: "needs-you",
48
- completed: "done", complete: "done", success: "done", succeeded: "done", shipped: "done",
49
- error: "failed", errored: "failed",
50
- cancelled: "stopped", canceled: "stopped", aborted: "stopped",
48
+ winner: "done", completed: "done", complete: "done", success: "done", succeeded: "done", shipped: "done",
49
+ error: "failed", errored: "failed", "driver-failed": "failed",
50
+ down: "stopped", "no-winner": "stopped", cancelled: "stopped", canceled: "stopped", aborted: "stopped",
51
51
  };
52
52
 
53
53
  /** Any status string as a `RunStatus`; a value nothing names reads as Unknown, never as a guess. */
@@ -1,171 +0,0 @@
1
- import { render, renderHook, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { beforeAll, describe, expect, it, vi } from "vitest";
4
- import type { SessionMessage } from "../types/message";
5
- import type { SessionPart, TextPart, ToolPart } from "../types/parts";
6
- import { useRunGroups } from "../hooks/use-run-groups";
7
- import { ChatContainer, type ChatContainerProps } from "./chat-container";
8
-
9
- // Only the missing browser layout API is stubbed; transcript components are real.
10
- beforeAll(() => { Element.prototype.scrollIntoView = vi.fn(); });
11
-
12
- const messages: SessionMessage[] = [
13
- { id: "user", role: "user" },
14
- { id: "assistant", role: "assistant" },
15
- ];
16
- const text = (value: string): TextPart => ({ type: "text", text: value });
17
- const note = (value: string): TextPart => ({ ...text(value), synthetic: true });
18
- const read = (id: string, path: string): ToolPart => ({
19
- type: "tool", id, tool: "read",
20
- state: { status: "completed", input: { path }, output: "File read." },
21
- });
22
- const firstTool = read("first", "src/first.ts");
23
- const secondTool = read("second", "src/second.ts");
24
-
25
- function expectOrder(...values: string[]) {
26
- const nodes = values.map((value) => screen.getByText(value));
27
- for (let i = 1; i < nodes.length; i++) {
28
- expect(nodes[i - 1].compareDocumentPosition(nodes[i]) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
29
- }
30
- }
31
-
32
- describe.each(["runs", "timeline"] as const)("synthetic transcript text: %s", (presentation) => {
33
- function view(partMap: Record<string, SessionPart[]>, extra: Partial<ChatContainerProps> = {}) {
34
- return <ChatContainer messages={messages} partMap={partMap} isStreaming={false} presentation={presentation} {...extra} />;
35
- }
36
-
37
- it("keeps mixed user and assistant notes in source order, outside speaker bubbles", () => {
38
- const { container } = render(view({
39
- user: [text("User before"), note("Delivery receipt"), text("User after")],
40
- assistant: [text("Assistant before"), note("Missing trace"), text("Assistant after")],
41
- }));
42
- expectOrder("User before", "Delivery receipt", "User after", "Assistant before", "Missing trace", "Assistant after");
43
- const notes = screen.getAllByRole("note", { name: "Application note" });
44
- expect(notes).toHaveLength(2);
45
- for (const element of notes) {
46
- expect(element.closest('.bg-foreground, .tangle-prose, [class*="rounded-[26px]"]')).toBeNull();
47
- }
48
- const bubbles = container.querySelectorAll(".bg-foreground.text-background");
49
- expect(bubbles).toHaveLength(2);
50
- for (const bubble of bubbles) expect(bubble).not.toHaveTextContent("Delivery receipt");
51
- });
52
-
53
- it("retains synthetic-only messages without inventing speaker text", () => {
54
- const { container } = render(view({ user: [note("Request receipt")], assistant: [note("No response persisted")] }));
55
- expectOrder("Request receipt", "No response persisted");
56
- expect(screen.getAllByRole("note")).toHaveLength(2);
57
- expect(screen.queryByText("Agent")).not.toBeInTheDocument();
58
- expect(container.querySelector(".bg-foreground, .tangle-prose")).toBeNull();
59
- });
60
-
61
- it("omits blank notes and blank source text", () => {
62
- render(view({ user: [note(" \n "), text(" ")], assistant: [note("\t"), text("\n")] }));
63
- expect(screen.queryByRole("note")).not.toBeInTheDocument();
64
- expect(screen.queryByText("Agent")).not.toBeInTheDocument();
65
- });
66
-
67
- it("does not parse application notes as Markdown, HTML, or OpenUI", () => {
68
- const literal = '<em>literal</em>\n**not markdown**\n```json\n{"type":"heading","text":"Not a heading"}\n```';
69
- render(view({ user: [], assistant: [note(literal)] }));
70
- const element = screen.getByRole("note");
71
- expect(element.textContent).toContain(literal);
72
- expect(element.querySelector("em, strong, h1, h2, h3, button, a")).toBeNull();
73
- expect(element.innerHTML).toContain("&lt;em&gt;literal&lt;/em&gt;");
74
- });
75
-
76
- it("renders notes even when OpenUI rendering is disabled", () => {
77
- render(view({ user: [], assistant: [note("Receipt with OpenUI off")] }, { enableOpenUI: false }));
78
- expect(screen.getByRole("note")).toHaveTextContent("Receipt with OpenUI off");
79
- });
80
-
81
- it("retains repeated notes and their position rather than deduplicating content", () => {
82
- render(view({ user: [], assistant: [note("Same receipt"), text("Between receipts"), note("Same receipt")] }));
83
- const notes = screen.getAllByRole("note");
84
- expect(notes).toHaveLength(2);
85
- const middle = screen.getByText("Between receipts");
86
- expect(notes[0].compareDocumentPosition(middle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
87
- expect(middle.compareDocumentPosition(notes[1]) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
88
- });
89
-
90
- it("preserves tool/note order and original tool callback arguments", async () => {
91
- const user = userEvent.setup();
92
- const activated = vi.fn();
93
- const renderRunTool = vi.fn<NonNullable<ChatContainerProps["renderToolActions"]>>((part, context) => (
94
- <button type="button" onClick={() => activated(part, context)}>Inspect {part.id}</button>
95
- ));
96
- const renderTimelineTool = vi.fn((part: ToolPart) => (
97
- <button type="button" onClick={() => activated(part)}>Inspect {part.id}</button>
98
- ));
99
- render(view({ user: [text("Read files")], assistant: [firstTool, note("Partial trace"), secondTool, text("Source answer")] }, {
100
- renderToolActions: renderRunTool,
101
- renderTimelineToolActions: renderTimelineTool,
102
- }));
103
- expectOrder("src/first.ts", "Partial trace", "src/second.ts", "Source answer");
104
- await user.click(screen.getByRole("button", { name: "Inspect second" }));
105
- expect(activated.mock.calls[0][0]).toBe(secondTool);
106
- if (presentation === "runs") {
107
- expect(activated.mock.calls[0][1]).toMatchObject({ messageId: "assistant", partIndex: 2 });
108
- expect(renderTimelineTool).not.toHaveBeenCalled();
109
- expect(screen.getByText("2 tools, 1 response")).toBeInTheDocument();
110
- } else {
111
- expect(renderRunTool).not.toHaveBeenCalled();
112
- }
113
- });
114
-
115
- it("does not treat an application receipt as the pending assistant reply", () => {
116
- const partMap = { user: [text("Question")], assistant: [note("Request accepted")] };
117
- const { rerender } = render(view(partMap, { isStreaming: true }));
118
- expect(screen.getByRole("note")).toHaveTextContent("Request accepted");
119
- if (presentation === "timeline") {
120
- expect(screen.getByRole("status", { name: "Agent is thinking" })).toBeInTheDocument();
121
- } else {
122
- expect(screen.getByText("Thinking")).toBeInTheDocument();
123
- }
124
- rerender(view({ ...partMap, assistant: [note("Request accepted"), text("Answer arrived")] }));
125
- expect(screen.getByRole("note")).toHaveTextContent("Request accepted");
126
- expect(screen.getByText("Answer arrived")).toBeInTheDocument();
127
- expect(screen.queryByRole("status", { name: "Agent is thinking" })).not.toBeInTheDocument();
128
- expect(screen.queryByText("Thinking")).not.toBeInTheDocument();
129
- });
130
- });
131
-
132
- describe("run attribution and manual collapse", () => {
133
- it.each([false, true])("excludes notes from response statistics and summaries (only notes: %s)", (onlyNotes) => {
134
- const { result } = renderHook(() => useRunGroups({
135
- messages,
136
- partMap: { user: [text("Question")], assistant: onlyNotes ? [note("App receipt")] : [text("Authored answer"), note("App receipt")] },
137
- isStreaming: false,
138
- }));
139
- const group = result.current.find((entry) => entry.type === "run");
140
- expect(group?.type).toBe("run");
141
- if (group?.type !== "run") throw new Error("Expected the real run group");
142
- expect(group.run.stats.textPartCount).toBe(onlyNotes ? 0 : 1);
143
- expect(group.run.summaryText).toBe(onlyNotes ? null : "Authored answer");
144
- });
145
-
146
- it("restores note order on keyboard expansion, without quoting notes in the run summary", async () => {
147
- const user = userEvent.setup();
148
- render(<ChatContainer messages={messages} partMap={{ user: [text("Question")], assistant: [firstTool, note("App-only receipt"), text("Source answer")] }} isStreaming={false} />);
149
- expectOrder("src/first.ts", "App-only receipt", "Source answer");
150
- const toggle = screen.getByRole("button", { expanded: true, name: /Agent/ });
151
- await user.click(toggle);
152
- expect(toggle).toHaveAttribute("aria-expanded", "false");
153
- expect(toggle).toHaveTextContent("1 tool, 1 response");
154
- expect(toggle).not.toHaveTextContent("App-only receipt");
155
- expect(screen.queryByRole("note")).not.toBeInTheDocument();
156
- toggle.focus();
157
- await user.keyboard("{Enter}");
158
- expect(toggle).toHaveAttribute("aria-expanded", "true");
159
- expectOrder("src/first.ts", "App-only receipt", "Source answer");
160
- });
161
-
162
- it("restores notes behind the existing explicit timeline limit", async () => {
163
- const user = userEvent.setup();
164
- render(<ChatContainer messages={messages} partMap={{ user: [text("Question")], assistant: [firstTool, note("App-only receipt"), text("Source answer")] }} isStreaming={false} presentation="timeline" collapseTimelineAfter={1} />);
165
- expect(screen.queryByRole("note")).not.toBeInTheDocument();
166
- await user.click(screen.getByRole("button", { name: "Show 2 more steps" }));
167
- expectOrder("src/first.ts", "App-only receipt", "Source answer");
168
- await user.click(screen.getByRole("button", { name: "Show less" }));
169
- expect(screen.queryByRole("note")).not.toBeInTheDocument();
170
- });
171
- });
@@ -1,189 +0,0 @@
1
- import { render, screen } from "@testing-library/react"
2
- import { beforeAll, describe, expect, it, vi } from "vitest"
3
- import type { SessionMessage } from "../types/message"
4
- import type { SessionPart, ToolPart } from "../types/parts"
5
- import { ChatContainer } from "./chat-container"
6
-
7
- // jsdom has no layout engine; the auto-scroll hook calls scrollIntoView.
8
- beforeAll(() => {
9
- Element.prototype.scrollIntoView = vi.fn()
10
- })
11
-
12
- const messages: SessionMessage[] = [
13
- { id: "m1", role: "assistant" },
14
- ]
15
-
16
- const editPart: ToolPart = {
17
- type: "tool",
18
- id: "tool-1",
19
- tool: "edit",
20
- state: {
21
- status: "completed",
22
- input: { path: "src/batch-writer.ts" },
23
- output: "Applied 1 edit to src/batch-writer.ts",
24
- },
25
- }
26
-
27
- const partMap: Record<string, SessionPart[]> = { m1: [editPart] }
28
-
29
- describe("ChatContainer timeline tool rendering", () => {
30
- it("shows thinking as soon as a submitted user message is streaming", () => {
31
- render(
32
- <ChatContainer
33
- messages={[{ id: "user-1", role: "user" }]}
34
- partMap={{
35
- "user-1": [{ type: "text", text: "hello" }],
36
- }}
37
- isStreaming
38
- presentation="timeline"
39
- />,
40
- )
41
-
42
- expect(
43
- screen.getByRole("status", { name: "Agent is thinking" }),
44
- ).toBeInTheDocument()
45
- })
46
-
47
- it("does not let an earlier assistant reply hide the next turn's thinking state", () => {
48
- render(
49
- <ChatContainer
50
- messages={[
51
- { id: "user-1", role: "user" },
52
- { id: "assistant-1", role: "assistant" },
53
- { id: "user-2", role: "user" },
54
- ]}
55
- partMap={{
56
- "user-1": [{ type: "text", text: "first" }],
57
- "assistant-1": [{ type: "text", text: "done" }],
58
- "user-2": [{ type: "text", text: "second" }],
59
- }}
60
- isStreaming
61
- presentation="timeline"
62
- />,
63
- )
64
-
65
- expect(
66
- screen.getByRole("status", { name: "Agent is thinking" }),
67
- ).toBeInTheDocument()
68
- })
69
-
70
- it("replaces the generic thinking row when the current reply has text", () => {
71
- render(
72
- <ChatContainer
73
- messages={[
74
- { id: "user-1", role: "user" },
75
- { id: "assistant-1", role: "assistant" },
76
- ]}
77
- partMap={{
78
- "user-1": [{ type: "text", text: "hello" }],
79
- "assistant-1": [{ type: "text", text: "working on it" }],
80
- }}
81
- isStreaming
82
- presentation="timeline"
83
- />,
84
- )
85
-
86
- expect(
87
- screen.queryByRole("status", { name: "Agent is thinking" }),
88
- ).not.toBeInTheDocument()
89
- expect(screen.getByText("working on it")).toBeInTheDocument()
90
- })
91
-
92
- it("shows a clean tool detail (the file path), not the raw input JSON", () => {
93
- render(
94
- <ChatContainer
95
- messages={messages}
96
- partMap={partMap}
97
- isStreaming={false}
98
- presentation="timeline"
99
- />,
100
- )
101
-
102
- expect(screen.getByText("src/batch-writer.ts")).toBeInTheDocument()
103
- // The raw `{ "path": ... }` input JSON must not leak into the summary row.
104
- expect(screen.queryByText(/"path"/)).not.toBeInTheDocument()
105
- })
106
-
107
- it("titles the tool row with the bare verb and prints the path once", () => {
108
- const { container } = render(
109
- <ChatContainer
110
- messages={messages}
111
- partMap={partMap}
112
- isStreaming={false}
113
- presentation="timeline"
114
- />,
115
- )
116
-
117
- expect(screen.getByText("Edit")).toBeInTheDocument()
118
- expect(screen.getAllByText("src/batch-writer.ts")).toHaveLength(1)
119
- expect(container.textContent).not.toContain("Edit src/batch-writer.ts")
120
- })
121
-
122
- it("stacks consecutive tool calls with no visible group header", () => {
123
- const readPart: ToolPart = {
124
- type: "tool",
125
- id: "tool-2",
126
- tool: "read",
127
- state: { status: "completed", input: { path: "src/lib/jitter.ts" }, output: "ok" },
128
- }
129
- render(
130
- <ChatContainer
131
- messages={messages}
132
- partMap={{ m1: [editPart, readPart] }}
133
- isStreaming={false}
134
- presentation="timeline"
135
- />,
136
- )
137
-
138
- expect(screen.queryByText(/tool activity/i)).not.toBeInTheDocument()
139
- expect(screen.getByRole("group", { name: "Tool activity" })).toBeInTheDocument()
140
- expect(screen.getByText("Edit")).toBeInTheDocument()
141
- expect(screen.getByText("Read")).toBeInTheDocument()
142
- })
143
-
144
- it("shimmers a running tool's verb and shows no spinner", () => {
145
- const running: ToolPart = {
146
- type: "tool",
147
- id: "tool-3",
148
- tool: "bash",
149
- state: { status: "running", input: { command: "pnpm build" }, time: { start: 1 } },
150
- }
151
- const { container } = render(
152
- <ChatContainer
153
- messages={[{ id: "user-1", role: "user" }, { id: "m1", role: "assistant" }]}
154
- partMap={{ "user-1": [{ type: "text", text: "build" }], m1: [running] }}
155
- isStreaming
156
- presentation="timeline"
157
- />,
158
- )
159
-
160
- const shimmer = container.querySelector(".tangle-text-shimmer")
161
- expect(shimmer).not.toBeNull()
162
- expect(shimmer!.textContent).toBe("Shell")
163
- expect(container.querySelector(".animate-spin")).toBeNull()
164
- })
165
-
166
- it("threads renderTimelineToolActions to the timeline tool item with its source part", () => {
167
- const renderTimelineToolActions = vi.fn((part: ToolPart) => (
168
- <button type="button">Open {part.id}</button>
169
- ))
170
-
171
- render(
172
- <ChatContainer
173
- messages={messages}
174
- partMap={partMap}
175
- isStreaming={false}
176
- presentation="timeline"
177
- renderTimelineToolActions={renderTimelineToolActions}
178
- />,
179
- )
180
-
181
- expect(renderTimelineToolActions).toHaveBeenCalled()
182
- expect(renderTimelineToolActions.mock.calls[0][0]).toMatchObject({
183
- id: "tool-1",
184
- })
185
- expect(
186
- screen.getByRole("button", { name: /open tool-1/i }),
187
- ).toBeInTheDocument()
188
- })
189
- })