@tangle-network/ui 1.0.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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
- package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
- package/dist/auth.d.ts +74 -0
- package/dist/auth.js +15 -0
- package/dist/button-CMQuQEW_.d.ts +17 -0
- package/dist/chat.d.ts +232 -0
- package/dist/chat.js +30 -0
- package/dist/chunk-2NFQRQOD.js +1009 -0
- package/dist/chunk-2VH6PUXD.js +186 -0
- package/dist/chunk-34A66VBG.js +214 -0
- package/dist/chunk-3OI2QKFD.js +0 -0
- package/dist/chunk-4CLN43XT.js +45 -0
- package/dist/chunk-54SQQMMM.js +156 -0
- package/dist/chunk-5Z5ZYMOJ.js +0 -0
- package/dist/chunk-66BNMOVT.js +167 -0
- package/dist/chunk-6BGQA4BQ.js +0 -0
- package/dist/chunk-7UO2ZMRQ.js +133 -0
- package/dist/chunk-BX6AQMUS.js +183 -0
- package/dist/chunk-CD53GZOM.js +59 -0
- package/dist/chunk-CSAIKY36.js +54 -0
- package/dist/chunk-EEE55AVS.js +1201 -0
- package/dist/chunk-GYPQXTJU.js +230 -0
- package/dist/chunk-HFL6R6IF.js +37 -0
- package/dist/chunk-HJKCSXCH.js +737 -0
- package/dist/chunk-LISXUB4D.js +1222 -0
- package/dist/chunk-LQS34IGP.js +0 -0
- package/dist/chunk-MKTSMWVD.js +109 -0
- package/dist/chunk-NKDZ7GZE.js +192 -0
- package/dist/chunk-OEX7NZE3.js +321 -0
- package/dist/chunk-Q56BYXQF.js +61 -0
- package/dist/chunk-Q7EIIWTC.js +0 -0
- package/dist/chunk-REJESC5U.js +117 -0
- package/dist/chunk-RQGKSCEZ.js +0 -0
- package/dist/chunk-RQHJBTEU.js +10 -0
- package/dist/chunk-TMFOPHHN.js +299 -0
- package/dist/chunk-XGKULLYE.js +40 -0
- package/dist/chunk-XIHMJ7ZQ.js +614 -0
- package/dist/chunk-YJ2G3XO5.js +1048 -0
- package/dist/chunk-YNN4O57I.js +754 -0
- package/dist/code-block-DjXf8eOG.d.ts +19 -0
- package/dist/document-editor-pane-A5LT5H4N.js +12 -0
- package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
- package/dist/editor.d.ts +120 -0
- package/dist/editor.js +34 -0
- package/dist/files.d.ts +175 -0
- package/dist/files.js +20 -0
- package/dist/hooks.d.ts +56 -0
- package/dist/hooks.js +41 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +446 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +14 -0
- package/dist/message-BHWbxBtT.d.ts +15 -0
- package/dist/openui.d.ts +115 -0
- package/dist/openui.js +12 -0
- package/dist/parts-dj7AcUg0.d.ts +36 -0
- package/dist/primitives.d.ts +332 -0
- package/dist/primitives.js +191 -0
- package/dist/run-PfLmDAox.d.ts +41 -0
- package/dist/run.d.ts +69 -0
- package/dist/run.js +36 -0
- package/dist/sdk-hooks.d.ts +285 -0
- package/dist/sdk-hooks.js +31 -0
- package/dist/stores.d.ts +17 -0
- package/dist/stores.js +76 -0
- package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
- package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
- package/dist/tool-previews.d.ts +48 -0
- package/dist/tool-previews.js +21 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +45 -0
- package/dist/utils.js +32 -0
- package/package.json +193 -0
- package/src/auth/auth.tsx +228 -0
- package/src/auth/index.ts +13 -0
- package/src/auth/login-layout.tsx +46 -0
- package/src/chat/agent-timeline.stories.tsx +429 -0
- package/src/chat/agent-timeline.tsx +360 -0
- package/src/chat/chat-container.tsx +486 -0
- package/src/chat/chat-input.stories.tsx +142 -0
- package/src/chat/chat-input.tsx +389 -0
- package/src/chat/chat-message.stories.tsx +237 -0
- package/src/chat/chat-message.tsx +129 -0
- package/src/chat/index.ts +18 -0
- package/src/chat/message-list.stories.tsx +336 -0
- package/src/chat/message-list.tsx +79 -0
- package/src/chat/thinking-indicator.stories.tsx +56 -0
- package/src/chat/thinking-indicator.tsx +30 -0
- package/src/chat/user-message.stories.tsx +92 -0
- package/src/chat/user-message.tsx +43 -0
- package/src/editor/document-editor-pane.tsx +351 -0
- package/src/editor/editor-provider.tsx +428 -0
- package/src/editor/editor-toolbar.tsx +130 -0
- package/src/editor/index.ts +31 -0
- package/src/editor/markdown-conversion.ts +21 -0
- package/src/editor/markdown-document-editor.tsx +137 -0
- package/src/editor/tiptap-editor.tsx +331 -0
- package/src/editor/use-editor.ts +221 -0
- package/src/files/file-artifact-pane.tsx +183 -0
- package/src/files/file-preview.tsx +342 -0
- package/src/files/file-tabs.tsx +71 -0
- package/src/files/file-tree.tsx +258 -0
- package/src/files/index.ts +17 -0
- package/src/files/rich-file-tree.stories.tsx +104 -0
- package/src/files/rich-file-tree.test.tsx +42 -0
- package/src/files/rich-file-tree.tsx +232 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-auth.ts +153 -0
- package/src/hooks/use-auto-scroll.ts +59 -0
- package/src/hooks/use-dropdown-menu.ts +40 -0
- package/src/hooks/use-live-time.test.tsx +40 -0
- package/src/hooks/use-live-time.ts +27 -0
- package/src/hooks/use-realtime-session.ts +319 -0
- package/src/hooks/use-run-collapse-state.ts +25 -0
- package/src/hooks/use-run-groups.ts +111 -0
- package/src/hooks/use-sdk-session.ts +575 -0
- package/src/hooks/use-sse-stream.ts +475 -0
- package/src/hooks/use-tool-call-stream.ts +96 -0
- package/src/index.ts +14 -0
- package/src/lib/utils.ts +6 -0
- package/src/markdown/code-block.tsx +198 -0
- package/src/markdown/index.ts +2 -0
- package/src/markdown/markdown.stories.tsx +190 -0
- package/src/markdown/markdown.tsx +62 -0
- package/src/openui/index.ts +20 -0
- package/src/openui/openui-artifact-renderer.tsx +542 -0
- package/src/primitives/artifact-pane.tsx +91 -0
- package/src/primitives/avatar.stories.tsx +95 -0
- package/src/primitives/avatar.tsx +47 -0
- package/src/primitives/badge.stories.tsx +57 -0
- package/src/primitives/badge.tsx +97 -0
- package/src/primitives/button.stories.tsx +48 -0
- package/src/primitives/button.tsx +115 -0
- package/src/primitives/card.stories.tsx +53 -0
- package/src/primitives/card.tsx +98 -0
- package/src/primitives/code-block.stories.tsx +115 -0
- package/src/primitives/code-block.tsx +22 -0
- package/src/primitives/design-tokens.stories.tsx +162 -0
- package/src/primitives/dialog.stories.tsx +176 -0
- package/src/primitives/dialog.tsx +137 -0
- package/src/primitives/drop-zone.stories.tsx +123 -0
- package/src/primitives/drop-zone.tsx +131 -0
- package/src/primitives/dropdown-menu.stories.tsx +122 -0
- package/src/primitives/dropdown-menu.tsx +214 -0
- package/src/primitives/empty-state.stories.tsx +81 -0
- package/src/primitives/empty-state.tsx +40 -0
- package/src/primitives/index.ts +118 -0
- package/src/primitives/input.stories.tsx +113 -0
- package/src/primitives/input.tsx +136 -0
- package/src/primitives/label.stories.tsx +84 -0
- package/src/primitives/label.tsx +24 -0
- package/src/primitives/progress.stories.tsx +93 -0
- package/src/primitives/progress.tsx +50 -0
- package/src/primitives/segmented-control.test.tsx +328 -0
- package/src/primitives/segmented-control.tsx +154 -0
- package/src/primitives/select.stories.tsx +164 -0
- package/src/primitives/select.tsx +158 -0
- package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
- package/src/primitives/sidebar-drop-zone.tsx +149 -0
- package/src/primitives/skeleton.stories.tsx +79 -0
- package/src/primitives/skeleton.tsx +55 -0
- package/src/primitives/stat-card.stories.tsx +137 -0
- package/src/primitives/stat-card.tsx +97 -0
- package/src/primitives/switch.stories.tsx +85 -0
- package/src/primitives/switch.tsx +28 -0
- package/src/primitives/table.stories.tsx +170 -0
- package/src/primitives/table.tsx +116 -0
- package/src/primitives/tabs.stories.tsx +180 -0
- package/src/primitives/tabs.tsx +71 -0
- package/src/primitives/terminal-display.stories.tsx +191 -0
- package/src/primitives/terminal-display.tsx +189 -0
- package/src/primitives/theme-toggle.stories.tsx +32 -0
- package/src/primitives/theme-toggle.tsx +96 -0
- package/src/primitives/toast.stories.tsx +155 -0
- package/src/primitives/toast.tsx +190 -0
- package/src/primitives/upload-progress.stories.tsx +120 -0
- package/src/primitives/upload-progress.tsx +110 -0
- package/src/run/expanded-tool-detail.stories.tsx +182 -0
- package/src/run/expanded-tool-detail.tsx +186 -0
- package/src/run/index.ts +13 -0
- package/src/run/inline-thinking-item.stories.tsx +136 -0
- package/src/run/inline-thinking-item.tsx +120 -0
- package/src/run/inline-tool-item.stories.tsx +222 -0
- package/src/run/inline-tool-item.tsx +190 -0
- package/src/run/run-group.stories.tsx +322 -0
- package/src/run/run-group.tsx +569 -0
- package/src/run/run-item-primitives.tsx +17 -0
- package/src/run/tool-call-feed.stories.tsx +294 -0
- package/src/run/tool-call-feed.tsx +192 -0
- package/src/run/tool-call-step.stories.tsx +198 -0
- package/src/run/tool-call-step.tsx +240 -0
- package/src/sdk-hooks.ts +38 -0
- package/src/stores/active-sessions-store.ts +455 -0
- package/src/stores/chat-store.ts +43 -0
- package/src/stores/index.ts +2 -0
- package/src/tool-previews/command-preview.tsx +116 -0
- package/src/tool-previews/diff-preview.tsx +85 -0
- package/src/tool-previews/glob-results-preview.tsx +98 -0
- package/src/tool-previews/grep-results-preview.tsx +157 -0
- package/src/tool-previews/index.ts +22 -0
- package/src/tool-previews/preview-primitives.tsx +84 -0
- package/src/tool-previews/question-preview.tsx +101 -0
- package/src/tool-previews/web-search-preview.tsx +117 -0
- package/src/tool-previews/write-file-preview.tsx +80 -0
- package/src/types/branding.ts +11 -0
- package/src/types/index.ts +5 -0
- package/src/types/message.ts +13 -0
- package/src/types/parts.ts +51 -0
- package/src/types/run.ts +56 -0
- package/src/types/tool-display.ts +41 -0
- package/src/utils/copy-text.ts +30 -0
- package/src/utils/format.test.ts +43 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/time-ago.ts +9 -0
- package/src/utils/tool-display.ts +238 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatMessage — single message bubble in the conversation.
|
|
3
|
+
*
|
|
4
|
+
* Supports user messages (plain text) and assistant messages
|
|
5
|
+
* (rich markdown with inline tool call activity).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type ReactNode } from "react";
|
|
9
|
+
import { User, Bot } from "lucide-react";
|
|
10
|
+
import { cn } from "../lib/utils";
|
|
11
|
+
import { Markdown } from "../markdown/markdown";
|
|
12
|
+
|
|
13
|
+
export type MessageRole = "user" | "assistant" | "system";
|
|
14
|
+
|
|
15
|
+
export interface ChatMessageProps {
|
|
16
|
+
role: MessageRole;
|
|
17
|
+
content: string;
|
|
18
|
+
/** Inline tool call activity rendered between text chunks */
|
|
19
|
+
toolCalls?: ReactNode;
|
|
20
|
+
/** Whether the message is still streaming */
|
|
21
|
+
isStreaming?: boolean;
|
|
22
|
+
/** Timestamp */
|
|
23
|
+
timestamp?: Date;
|
|
24
|
+
className?: string;
|
|
25
|
+
/** Custom user label. Default: "You" */
|
|
26
|
+
userLabel?: string;
|
|
27
|
+
/** Custom assistant label. Default: "Agent" */
|
|
28
|
+
assistantLabel?: string;
|
|
29
|
+
/** Hide the role label row entirely */
|
|
30
|
+
hideRoleLabel?: boolean;
|
|
31
|
+
/** Hide the avatar icon */
|
|
32
|
+
hideAvatar?: boolean;
|
|
33
|
+
/** Custom avatar element (replaces default User/Bot icon) */
|
|
34
|
+
avatar?: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ChatMessage({
|
|
38
|
+
role,
|
|
39
|
+
content,
|
|
40
|
+
toolCalls,
|
|
41
|
+
isStreaming,
|
|
42
|
+
timestamp,
|
|
43
|
+
className,
|
|
44
|
+
userLabel = "You",
|
|
45
|
+
assistantLabel = "Agent",
|
|
46
|
+
hideRoleLabel,
|
|
47
|
+
hideAvatar,
|
|
48
|
+
avatar,
|
|
49
|
+
}: ChatMessageProps) {
|
|
50
|
+
const isUser = role === "user";
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
className={cn(
|
|
55
|
+
"flex gap-3",
|
|
56
|
+
isUser ? "flex-row-reverse" : "flex-row",
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
{/* Avatar */}
|
|
61
|
+
{!hideAvatar && (
|
|
62
|
+
avatar ? (
|
|
63
|
+
<div className="mt-0.5 shrink-0">{avatar}</div>
|
|
64
|
+
) : (
|
|
65
|
+
<div
|
|
66
|
+
className={cn(
|
|
67
|
+
"mt-0.5 flex shrink-0 items-center justify-center rounded-[calc(var(--radius-md)+2px)] border",
|
|
68
|
+
"h-[var(--avatar-size)] w-[var(--avatar-size)]",
|
|
69
|
+
isUser
|
|
70
|
+
? "border-border bg-[var(--accent-surface-soft)] text-[var(--accent-text)]"
|
|
71
|
+
: "border-border bg-muted text-[var(--brand-cool)]",
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{isUser ? <User className="h-3.5 w-3.5" /> : <Bot className="h-3.5 w-3.5" />}
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{/* Bubble */}
|
|
80
|
+
<div
|
|
81
|
+
className={cn(
|
|
82
|
+
"min-w-0 max-w-[85%] space-y-1 rounded-[var(--radius-lg)] border",
|
|
83
|
+
"px-[var(--chat-message-px)] py-[var(--chat-message-py)]",
|
|
84
|
+
isUser
|
|
85
|
+
? "border-border bg-muted/50"
|
|
86
|
+
: "border-border bg-card",
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{/* Role label + timestamp */}
|
|
90
|
+
{!hideRoleLabel && (
|
|
91
|
+
<div className={cn("flex items-center gap-2", isUser && "flex-row-reverse")}>
|
|
92
|
+
<span className="text-[var(--font-size-xs)] font-[var(--chat-label-weight,600)] uppercase tracking-[var(--chat-label-tracking,0.14em)] text-foreground">
|
|
93
|
+
{isUser ? userLabel : assistantLabel}
|
|
94
|
+
</span>
|
|
95
|
+
{timestamp && (
|
|
96
|
+
<span className="text-[var(--font-size-xs)] text-muted-foreground">
|
|
97
|
+
{formatTime(timestamp)}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
{/* Message body */}
|
|
104
|
+
{isUser ? (
|
|
105
|
+
<div className="whitespace-pre-wrap text-[var(--font-size-base)] leading-[var(--line-height-base)] text-foreground">
|
|
106
|
+
{content}
|
|
107
|
+
</div>
|
|
108
|
+
) : (
|
|
109
|
+
<>
|
|
110
|
+
{content && <Markdown className="tangle-prose text-[var(--font-size-base)] leading-[var(--line-height-base)]">{content}</Markdown>}
|
|
111
|
+
{isStreaming && (
|
|
112
|
+
<span className="ml-0.5 inline-block h-4 w-2 animate-pulse rounded-sm bg-[var(--brand-cool)] align-text-bottom" />
|
|
113
|
+
)}
|
|
114
|
+
</>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{/* Inline tool calls (left-aligned below agent text) */}
|
|
118
|
+
{toolCalls}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatTime(date: Date): string {
|
|
125
|
+
return date.toLocaleTimeString(undefined, {
|
|
126
|
+
hour: "numeric",
|
|
127
|
+
minute: "2-digit",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { ChatContainer, type ChatContainerProps } from "./chat-container";
|
|
2
|
+
export { MessageList, type MessageListProps } from "./message-list";
|
|
3
|
+
export { UserMessage, type UserMessageProps } from "./user-message";
|
|
4
|
+
export { ChatMessage, type ChatMessageProps, type MessageRole } from "./chat-message";
|
|
5
|
+
export { ChatInput, type ChatInputProps, type PendingFile } from "./chat-input";
|
|
6
|
+
export { ThinkingIndicator, type ThinkingIndicatorProps } from "./thinking-indicator";
|
|
7
|
+
export {
|
|
8
|
+
AgentTimeline,
|
|
9
|
+
type AgentTimelineProps,
|
|
10
|
+
type AgentTimelineItem,
|
|
11
|
+
type AgentTimelineMessageItem,
|
|
12
|
+
type AgentTimelineToolItem,
|
|
13
|
+
type AgentTimelineToolGroupItem,
|
|
14
|
+
type AgentTimelineStatusItem,
|
|
15
|
+
type AgentTimelineArtifactItem,
|
|
16
|
+
type AgentTimelineCustomItem,
|
|
17
|
+
type AgentTimelineTone,
|
|
18
|
+
} from "./agent-timeline";
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { MessageList } from './message-list'
|
|
4
|
+
import type { GroupedMessage } from '../types/run'
|
|
5
|
+
import type { SessionPart } from '../types/parts'
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof MessageList> = {
|
|
8
|
+
title: 'Chat/MessageList',
|
|
9
|
+
component: MessageList,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'fullscreen',
|
|
12
|
+
backgrounds: { default: 'dark' },
|
|
13
|
+
},
|
|
14
|
+
decorators: [
|
|
15
|
+
(Story) => (
|
|
16
|
+
<div className="min-h-screen bg-[var(--bg-root)] p-8">
|
|
17
|
+
<div className="mx-auto max-w-3xl">
|
|
18
|
+
<Story />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default meta
|
|
26
|
+
type Story = StoryObj<typeof MessageList>
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Mock data — a realistic agent coding session
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const NOW = Date.now()
|
|
33
|
+
const t = (offsetSeconds: number) => NOW - offsetSeconds * 1000
|
|
34
|
+
|
|
35
|
+
// User messages
|
|
36
|
+
const userMsg1 = {
|
|
37
|
+
id: 'user-1',
|
|
38
|
+
role: 'user' as const,
|
|
39
|
+
time: { created: t(120) },
|
|
40
|
+
}
|
|
41
|
+
const userMsg2 = {
|
|
42
|
+
id: 'user-2',
|
|
43
|
+
role: 'user' as const,
|
|
44
|
+
time: { created: t(60) },
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Assistant messages (each run has one or more)
|
|
48
|
+
const assistantMsg1 = {
|
|
49
|
+
id: 'asst-1',
|
|
50
|
+
role: 'assistant' as const,
|
|
51
|
+
time: { created: t(115), completed: t(100) },
|
|
52
|
+
}
|
|
53
|
+
const assistantMsg2 = {
|
|
54
|
+
id: 'asst-2',
|
|
55
|
+
role: 'assistant' as const,
|
|
56
|
+
time: { created: t(55), completed: t(30) },
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const groups: GroupedMessage[] = [
|
|
60
|
+
{ type: 'user', message: userMsg1 },
|
|
61
|
+
{
|
|
62
|
+
type: 'run',
|
|
63
|
+
run: {
|
|
64
|
+
id: 'run-1',
|
|
65
|
+
messages: [assistantMsg1],
|
|
66
|
+
isComplete: true,
|
|
67
|
+
isStreaming: false,
|
|
68
|
+
summaryText: 'Read 3 files, identified the staleTime misconfiguration',
|
|
69
|
+
finalTextPart: {
|
|
70
|
+
messageId: 'asst-1',
|
|
71
|
+
partIndex: 3,
|
|
72
|
+
text: 'The issue is your default `staleTime` is 0.',
|
|
73
|
+
},
|
|
74
|
+
stats: {
|
|
75
|
+
toolCount: 3,
|
|
76
|
+
messageCount: 1,
|
|
77
|
+
thinkingDurationMs: 4200,
|
|
78
|
+
textPartCount: 1,
|
|
79
|
+
toolCategories: new Set(['read', 'search'] as const),
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{ type: 'user', message: userMsg2 },
|
|
84
|
+
{
|
|
85
|
+
type: 'run',
|
|
86
|
+
run: {
|
|
87
|
+
id: 'run-2',
|
|
88
|
+
messages: [assistantMsg2],
|
|
89
|
+
isComplete: true,
|
|
90
|
+
isStreaming: false,
|
|
91
|
+
summaryText: 'Patched QueryClient config and updated the staleTime',
|
|
92
|
+
finalTextPart: {
|
|
93
|
+
messageId: 'asst-2',
|
|
94
|
+
partIndex: 5,
|
|
95
|
+
text: 'Done — staleTime set to 60s, cache flicker resolved.',
|
|
96
|
+
},
|
|
97
|
+
stats: {
|
|
98
|
+
toolCount: 2,
|
|
99
|
+
messageCount: 1,
|
|
100
|
+
thinkingDurationMs: 2100,
|
|
101
|
+
textPartCount: 2,
|
|
102
|
+
toolCategories: new Set(['read', 'edit'] as const),
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
// Part map — parts keyed by message ID
|
|
109
|
+
const partMap: Record<string, SessionPart[]> = {
|
|
110
|
+
'user-1': [
|
|
111
|
+
{
|
|
112
|
+
type: 'text',
|
|
113
|
+
text: 'Can you help me debug why my React Query cache is not working? Every component mount triggers a refetch even for data that was just fetched.',
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
'user-2': [
|
|
117
|
+
{
|
|
118
|
+
type: 'text',
|
|
119
|
+
text: 'Perfect, please fix it — set staleTime to 60 seconds globally.',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
'asst-1': [
|
|
123
|
+
{
|
|
124
|
+
type: 'reasoning',
|
|
125
|
+
text: 'The user is experiencing stale data refetches on mount. Default staleTime in TanStack Query v5 is 0, which causes this. I should read their QueryClient setup to confirm.',
|
|
126
|
+
time: { start: t(115), end: t(112) },
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
type: 'tool',
|
|
130
|
+
id: 'tool-1',
|
|
131
|
+
tool: 'read',
|
|
132
|
+
callID: 'call-1',
|
|
133
|
+
state: {
|
|
134
|
+
status: 'completed',
|
|
135
|
+
input: { path: 'src/lib/query-client.ts' },
|
|
136
|
+
output: 'export const queryClient = new QueryClient()',
|
|
137
|
+
time: { start: t(112), end: t(111) },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: 'tool',
|
|
142
|
+
id: 'tool-2',
|
|
143
|
+
tool: 'grep',
|
|
144
|
+
callID: 'call-2',
|
|
145
|
+
state: {
|
|
146
|
+
status: 'completed',
|
|
147
|
+
input: { pattern: 'staleTime', path: 'src' },
|
|
148
|
+
output: 'No matches found.',
|
|
149
|
+
time: { start: t(111), end: t(110) },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: 'tool',
|
|
154
|
+
id: 'tool-3',
|
|
155
|
+
tool: 'glob',
|
|
156
|
+
callID: 'call-3',
|
|
157
|
+
state: {
|
|
158
|
+
status: 'completed',
|
|
159
|
+
input: { pattern: 'src/**/*.ts' },
|
|
160
|
+
output: 'src/lib/query-client.ts\nsrc/hooks/useData.ts\nsrc/app/layout.tsx',
|
|
161
|
+
time: { start: t(110), end: t(109) },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: 'text',
|
|
166
|
+
text: `The issue is your default \`staleTime\` is \`0\`, which is the TanStack Query v5 default. This means React Query considers all cached data immediately stale, triggering a background refetch on every component mount.
|
|
167
|
+
|
|
168
|
+
Your current setup:
|
|
169
|
+
|
|
170
|
+
\`\`\`typescript
|
|
171
|
+
export const queryClient = new QueryClient()
|
|
172
|
+
// No defaultOptions — staleTime defaults to 0ms
|
|
173
|
+
\`\`\`
|
|
174
|
+
|
|
175
|
+
No \`staleTime\` configuration was found anywhere in the codebase. Set it globally to fix the flicker.`,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
'asst-2': [
|
|
179
|
+
{
|
|
180
|
+
type: 'tool',
|
|
181
|
+
id: 'tool-4',
|
|
182
|
+
tool: 'read',
|
|
183
|
+
callID: 'call-4',
|
|
184
|
+
state: {
|
|
185
|
+
status: 'completed',
|
|
186
|
+
input: { path: 'src/lib/query-client.ts' },
|
|
187
|
+
output: 'export const queryClient = new QueryClient()',
|
|
188
|
+
time: { start: t(55), end: t(54) },
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
type: 'tool',
|
|
193
|
+
id: 'tool-5',
|
|
194
|
+
tool: 'edit',
|
|
195
|
+
callID: 'call-5',
|
|
196
|
+
state: {
|
|
197
|
+
status: 'completed',
|
|
198
|
+
input: {
|
|
199
|
+
path: 'src/lib/query-client.ts',
|
|
200
|
+
old_string: 'new QueryClient()',
|
|
201
|
+
new_string: 'new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 } } })',
|
|
202
|
+
},
|
|
203
|
+
output: 'File updated.',
|
|
204
|
+
time: { start: t(54), end: t(53) },
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
type: 'text',
|
|
209
|
+
text: `Done. Updated \`src/lib/query-client.ts\`:
|
|
210
|
+
|
|
211
|
+
\`\`\`typescript
|
|
212
|
+
export const queryClient = new QueryClient({
|
|
213
|
+
defaultOptions: {
|
|
214
|
+
queries: {
|
|
215
|
+
staleTime: 60_000, // 60 seconds
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
})
|
|
219
|
+
\`\`\`
|
|
220
|
+
|
|
221
|
+
Cached data is now considered fresh for 60 seconds. Components that mount within that window will read from cache without triggering a refetch.`,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Interactive wrapper — handles collapse state
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
function InteractiveMessageList({
|
|
231
|
+
groups: g,
|
|
232
|
+
partMap: pm,
|
|
233
|
+
}: {
|
|
234
|
+
groups: GroupedMessage[]
|
|
235
|
+
partMap: Record<string, SessionPart[]>
|
|
236
|
+
}) {
|
|
237
|
+
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
|
|
238
|
+
return (
|
|
239
|
+
<MessageList
|
|
240
|
+
groups={g}
|
|
241
|
+
partMap={pm}
|
|
242
|
+
isCollapsed={(runId) => collapsed[runId] ?? false}
|
|
243
|
+
onToggleCollapse={(runId) =>
|
|
244
|
+
setCollapsed((prev) => ({ ...prev, [runId]: !prev[runId] }))
|
|
245
|
+
}
|
|
246
|
+
/>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export const FullConversation: Story = {
|
|
251
|
+
render: () => <InteractiveMessageList groups={groups} partMap={partMap} />,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export const WithCollapsedRuns: Story = {
|
|
255
|
+
render: () => {
|
|
256
|
+
// Render with runs collapsed by default
|
|
257
|
+
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({
|
|
258
|
+
'run-1': true,
|
|
259
|
+
'run-2': true,
|
|
260
|
+
})
|
|
261
|
+
return (
|
|
262
|
+
<MessageList
|
|
263
|
+
groups={groups}
|
|
264
|
+
partMap={partMap}
|
|
265
|
+
isCollapsed={(runId) => collapsed[runId] ?? false}
|
|
266
|
+
onToggleCollapse={(runId) =>
|
|
267
|
+
setCollapsed((prev) => ({ ...prev, [runId]: !prev[runId] }))
|
|
268
|
+
}
|
|
269
|
+
/>
|
|
270
|
+
)
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export const StreamingRun: Story = {
|
|
275
|
+
render: () => {
|
|
276
|
+
const streamingGroups: GroupedMessage[] = [
|
|
277
|
+
{ type: 'user', message: userMsg1 },
|
|
278
|
+
{
|
|
279
|
+
type: 'run',
|
|
280
|
+
run: {
|
|
281
|
+
id: 'run-streaming',
|
|
282
|
+
messages: [assistantMsg1],
|
|
283
|
+
isComplete: false,
|
|
284
|
+
isStreaming: true,
|
|
285
|
+
summaryText: null,
|
|
286
|
+
finalTextPart: null,
|
|
287
|
+
stats: {
|
|
288
|
+
toolCount: 1,
|
|
289
|
+
messageCount: 1,
|
|
290
|
+
thinkingDurationMs: 0,
|
|
291
|
+
textPartCount: 0,
|
|
292
|
+
toolCategories: new Set(['read'] as const),
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
]
|
|
297
|
+
const streamingPartMap: Record<string, SessionPart[]> = {
|
|
298
|
+
'user-1': partMap['user-1'],
|
|
299
|
+
'asst-1': [
|
|
300
|
+
{
|
|
301
|
+
type: 'tool',
|
|
302
|
+
id: 'tool-1',
|
|
303
|
+
tool: 'read',
|
|
304
|
+
callID: 'call-1',
|
|
305
|
+
state: {
|
|
306
|
+
status: 'running',
|
|
307
|
+
input: { path: 'src/lib/query-client.ts' },
|
|
308
|
+
time: { start: t(2) },
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
}
|
|
313
|
+
return (
|
|
314
|
+
<MessageList
|
|
315
|
+
groups={streamingGroups}
|
|
316
|
+
partMap={streamingPartMap}
|
|
317
|
+
isCollapsed={() => false}
|
|
318
|
+
onToggleCollapse={() => {}}
|
|
319
|
+
/>
|
|
320
|
+
)
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export const OnlyUserMessages: Story = {
|
|
325
|
+
render: () => (
|
|
326
|
+
<MessageList
|
|
327
|
+
groups={[
|
|
328
|
+
{ type: 'user', message: userMsg1 },
|
|
329
|
+
{ type: 'user', message: userMsg2 },
|
|
330
|
+
]}
|
|
331
|
+
partMap={partMap}
|
|
332
|
+
isCollapsed={() => false}
|
|
333
|
+
onToggleCollapse={() => {}}
|
|
334
|
+
/>
|
|
335
|
+
),
|
|
336
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { memo, type ReactNode } from "react";
|
|
2
|
+
import type { GroupedMessage } from "../types/run";
|
|
3
|
+
import type { SessionPart } from "../types/parts";
|
|
4
|
+
import type { SessionMessage } from "../types/message";
|
|
5
|
+
import type { AgentBranding } from "../types/branding";
|
|
6
|
+
import type { CustomToolRenderer } from "../types/tool-display";
|
|
7
|
+
import type { ToolPart } from "../types/parts";
|
|
8
|
+
import { RunGroup } from "../run/run-group";
|
|
9
|
+
import { UserMessage } from "./user-message";
|
|
10
|
+
|
|
11
|
+
export interface MessageListProps {
|
|
12
|
+
groups: GroupedMessage[];
|
|
13
|
+
partMap: Record<string, SessionPart[]>;
|
|
14
|
+
isCollapsed: (runId: string) => boolean;
|
|
15
|
+
onToggleCollapse: (runId: string) => void;
|
|
16
|
+
branding?: AgentBranding;
|
|
17
|
+
renderToolDetail?: CustomToolRenderer;
|
|
18
|
+
renderRunActions?: (group: Extract<GroupedMessage, { type: "run" }>["run"]) => ReactNode;
|
|
19
|
+
renderUserMessageActions?: (message: SessionMessage, parts: SessionPart[]) => ReactNode;
|
|
20
|
+
renderToolActions?: (
|
|
21
|
+
part: ToolPart,
|
|
22
|
+
options: {
|
|
23
|
+
run: Extract<GroupedMessage, { type: "run" }>["run"];
|
|
24
|
+
messageId: string;
|
|
25
|
+
partIndex: number;
|
|
26
|
+
},
|
|
27
|
+
) => ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Maps GroupedMessage[] to UserMessage and RunGroup components.
|
|
32
|
+
* This is the main render list for the chat view.
|
|
33
|
+
*/
|
|
34
|
+
export const MessageList = memo(
|
|
35
|
+
({
|
|
36
|
+
groups,
|
|
37
|
+
partMap,
|
|
38
|
+
isCollapsed,
|
|
39
|
+
onToggleCollapse,
|
|
40
|
+
branding,
|
|
41
|
+
renderToolDetail,
|
|
42
|
+
renderRunActions,
|
|
43
|
+
renderUserMessageActions,
|
|
44
|
+
renderToolActions,
|
|
45
|
+
}: MessageListProps) => {
|
|
46
|
+
return (
|
|
47
|
+
<div className="space-y-4">
|
|
48
|
+
{groups.map((group) => {
|
|
49
|
+
if (group.type === "user") {
|
|
50
|
+
const messageParts = partMap[group.message.id] ?? [];
|
|
51
|
+
return (
|
|
52
|
+
<UserMessage
|
|
53
|
+
key={group.message.id}
|
|
54
|
+
message={group.message}
|
|
55
|
+
parts={messageParts}
|
|
56
|
+
actions={renderUserMessageActions?.(group.message, messageParts)}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<RunGroup
|
|
63
|
+
key={group.run.id}
|
|
64
|
+
run={group.run}
|
|
65
|
+
partMap={partMap}
|
|
66
|
+
collapsed={isCollapsed(group.run.id)}
|
|
67
|
+
onToggle={() => onToggleCollapse(group.run.id)}
|
|
68
|
+
branding={branding}
|
|
69
|
+
renderToolDetail={renderToolDetail}
|
|
70
|
+
headerActions={renderRunActions?.(group.run)}
|
|
71
|
+
renderToolActions={renderToolActions}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
MessageList.displayName = "MessageList";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { ThinkingIndicator } from './thinking-indicator'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ThinkingIndicator> = {
|
|
5
|
+
title: 'Chat/ThinkingIndicator',
|
|
6
|
+
component: ThinkingIndicator,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'fullscreen',
|
|
9
|
+
backgrounds: { default: 'dark' },
|
|
10
|
+
},
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<div className="min-h-screen bg-[var(--bg-root)] p-8">
|
|
14
|
+
<div className="mx-auto max-w-2xl">
|
|
15
|
+
<Story />
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
),
|
|
19
|
+
],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default meta
|
|
23
|
+
type Story = StoryObj<typeof ThinkingIndicator>
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Animated state — the component starts at elapsed=0 and ticks up.
|
|
27
|
+
* In Storybook you'll see the bouncing dots and text update live.
|
|
28
|
+
*/
|
|
29
|
+
export const Default: Story = {}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Show it inline below a chat message to simulate the agent responding.
|
|
33
|
+
*/
|
|
34
|
+
export const InContext: Story = {
|
|
35
|
+
render: () => (
|
|
36
|
+
<div className="space-y-1">
|
|
37
|
+
<div className="rounded-[calc(var(--radius-xl)+2px)] border border-border bg-card px-4 py-4 text-sm text-foreground">
|
|
38
|
+
Let me analyze the rate limiter implementation and check the Redis pipeline for atomicity issues.
|
|
39
|
+
</div>
|
|
40
|
+
<ThinkingIndicator />
|
|
41
|
+
</div>
|
|
42
|
+
),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Multiple indicators to show staggered animation timing.
|
|
47
|
+
*/
|
|
48
|
+
export const Stacked: Story = {
|
|
49
|
+
render: () => (
|
|
50
|
+
<div className="space-y-2">
|
|
51
|
+
<ThinkingIndicator />
|
|
52
|
+
<ThinkingIndicator className="opacity-60" />
|
|
53
|
+
<ThinkingIndicator className="opacity-30" />
|
|
54
|
+
</div>
|
|
55
|
+
),
|
|
56
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
export interface ThinkingIndicatorProps {
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function ThinkingIndicator({ className }: ThinkingIndicatorProps) {
|
|
9
|
+
const [elapsed, setElapsed] = useState(0);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const interval = window.setInterval(() => setElapsed((current) => current + 1), 1000);
|
|
13
|
+
return () => window.clearInterval(interval);
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className={cn("flex items-center gap-2 px-3 py-1.5", className)}>
|
|
18
|
+
<div className="flex gap-[3px]">
|
|
19
|
+
<span className="h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "0ms" }} />
|
|
20
|
+
<span className="h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "150ms" }} />
|
|
21
|
+
<span className="h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "300ms" }} />
|
|
22
|
+
</div>
|
|
23
|
+
{elapsed > 3 && (
|
|
24
|
+
<span className="text-[var(--font-size-xs)] tabular-nums text-[var(--text-dim)]">
|
|
25
|
+
{elapsed}s
|
|
26
|
+
</span>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|