@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.
Files changed (220) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -0
  4. package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
  5. package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
  6. package/dist/auth.d.ts +74 -0
  7. package/dist/auth.js +15 -0
  8. package/dist/button-CMQuQEW_.d.ts +17 -0
  9. package/dist/chat.d.ts +232 -0
  10. package/dist/chat.js +30 -0
  11. package/dist/chunk-2NFQRQOD.js +1009 -0
  12. package/dist/chunk-2VH6PUXD.js +186 -0
  13. package/dist/chunk-34A66VBG.js +214 -0
  14. package/dist/chunk-3OI2QKFD.js +0 -0
  15. package/dist/chunk-4CLN43XT.js +45 -0
  16. package/dist/chunk-54SQQMMM.js +156 -0
  17. package/dist/chunk-5Z5ZYMOJ.js +0 -0
  18. package/dist/chunk-66BNMOVT.js +167 -0
  19. package/dist/chunk-6BGQA4BQ.js +0 -0
  20. package/dist/chunk-7UO2ZMRQ.js +133 -0
  21. package/dist/chunk-BX6AQMUS.js +183 -0
  22. package/dist/chunk-CD53GZOM.js +59 -0
  23. package/dist/chunk-CSAIKY36.js +54 -0
  24. package/dist/chunk-EEE55AVS.js +1201 -0
  25. package/dist/chunk-GYPQXTJU.js +230 -0
  26. package/dist/chunk-HFL6R6IF.js +37 -0
  27. package/dist/chunk-HJKCSXCH.js +737 -0
  28. package/dist/chunk-LISXUB4D.js +1222 -0
  29. package/dist/chunk-LQS34IGP.js +0 -0
  30. package/dist/chunk-MKTSMWVD.js +109 -0
  31. package/dist/chunk-NKDZ7GZE.js +192 -0
  32. package/dist/chunk-OEX7NZE3.js +321 -0
  33. package/dist/chunk-Q56BYXQF.js +61 -0
  34. package/dist/chunk-Q7EIIWTC.js +0 -0
  35. package/dist/chunk-REJESC5U.js +117 -0
  36. package/dist/chunk-RQGKSCEZ.js +0 -0
  37. package/dist/chunk-RQHJBTEU.js +10 -0
  38. package/dist/chunk-TMFOPHHN.js +299 -0
  39. package/dist/chunk-XGKULLYE.js +40 -0
  40. package/dist/chunk-XIHMJ7ZQ.js +614 -0
  41. package/dist/chunk-YJ2G3XO5.js +1048 -0
  42. package/dist/chunk-YNN4O57I.js +754 -0
  43. package/dist/code-block-DjXf8eOG.d.ts +19 -0
  44. package/dist/document-editor-pane-A5LT5H4N.js +12 -0
  45. package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
  46. package/dist/editor.d.ts +120 -0
  47. package/dist/editor.js +34 -0
  48. package/dist/files.d.ts +175 -0
  49. package/dist/files.js +20 -0
  50. package/dist/hooks.d.ts +56 -0
  51. package/dist/hooks.js +41 -0
  52. package/dist/index.d.ts +43 -0
  53. package/dist/index.js +446 -0
  54. package/dist/markdown.d.ts +15 -0
  55. package/dist/markdown.js +14 -0
  56. package/dist/message-BHWbxBtT.d.ts +15 -0
  57. package/dist/openui.d.ts +115 -0
  58. package/dist/openui.js +12 -0
  59. package/dist/parts-dj7AcUg0.d.ts +36 -0
  60. package/dist/primitives.d.ts +332 -0
  61. package/dist/primitives.js +191 -0
  62. package/dist/run-PfLmDAox.d.ts +41 -0
  63. package/dist/run.d.ts +69 -0
  64. package/dist/run.js +36 -0
  65. package/dist/sdk-hooks.d.ts +285 -0
  66. package/dist/sdk-hooks.js +31 -0
  67. package/dist/stores.d.ts +17 -0
  68. package/dist/stores.js +76 -0
  69. package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
  70. package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
  71. package/dist/tool-previews.d.ts +48 -0
  72. package/dist/tool-previews.js +21 -0
  73. package/dist/types.d.ts +19 -0
  74. package/dist/types.js +1 -0
  75. package/dist/utils.d.ts +45 -0
  76. package/dist/utils.js +32 -0
  77. package/package.json +193 -0
  78. package/src/auth/auth.tsx +228 -0
  79. package/src/auth/index.ts +13 -0
  80. package/src/auth/login-layout.tsx +46 -0
  81. package/src/chat/agent-timeline.stories.tsx +429 -0
  82. package/src/chat/agent-timeline.tsx +360 -0
  83. package/src/chat/chat-container.tsx +486 -0
  84. package/src/chat/chat-input.stories.tsx +142 -0
  85. package/src/chat/chat-input.tsx +389 -0
  86. package/src/chat/chat-message.stories.tsx +237 -0
  87. package/src/chat/chat-message.tsx +129 -0
  88. package/src/chat/index.ts +18 -0
  89. package/src/chat/message-list.stories.tsx +336 -0
  90. package/src/chat/message-list.tsx +79 -0
  91. package/src/chat/thinking-indicator.stories.tsx +56 -0
  92. package/src/chat/thinking-indicator.tsx +30 -0
  93. package/src/chat/user-message.stories.tsx +92 -0
  94. package/src/chat/user-message.tsx +43 -0
  95. package/src/editor/document-editor-pane.tsx +351 -0
  96. package/src/editor/editor-provider.tsx +428 -0
  97. package/src/editor/editor-toolbar.tsx +130 -0
  98. package/src/editor/index.ts +31 -0
  99. package/src/editor/markdown-conversion.ts +21 -0
  100. package/src/editor/markdown-document-editor.tsx +137 -0
  101. package/src/editor/tiptap-editor.tsx +331 -0
  102. package/src/editor/use-editor.ts +221 -0
  103. package/src/files/file-artifact-pane.tsx +183 -0
  104. package/src/files/file-preview.tsx +342 -0
  105. package/src/files/file-tabs.tsx +71 -0
  106. package/src/files/file-tree.tsx +258 -0
  107. package/src/files/index.ts +17 -0
  108. package/src/files/rich-file-tree.stories.tsx +104 -0
  109. package/src/files/rich-file-tree.test.tsx +42 -0
  110. package/src/files/rich-file-tree.tsx +232 -0
  111. package/src/hooks/index.ts +10 -0
  112. package/src/hooks/use-auth.ts +153 -0
  113. package/src/hooks/use-auto-scroll.ts +59 -0
  114. package/src/hooks/use-dropdown-menu.ts +40 -0
  115. package/src/hooks/use-live-time.test.tsx +40 -0
  116. package/src/hooks/use-live-time.ts +27 -0
  117. package/src/hooks/use-realtime-session.ts +319 -0
  118. package/src/hooks/use-run-collapse-state.ts +25 -0
  119. package/src/hooks/use-run-groups.ts +111 -0
  120. package/src/hooks/use-sdk-session.ts +575 -0
  121. package/src/hooks/use-sse-stream.ts +475 -0
  122. package/src/hooks/use-tool-call-stream.ts +96 -0
  123. package/src/index.ts +14 -0
  124. package/src/lib/utils.ts +6 -0
  125. package/src/markdown/code-block.tsx +198 -0
  126. package/src/markdown/index.ts +2 -0
  127. package/src/markdown/markdown.stories.tsx +190 -0
  128. package/src/markdown/markdown.tsx +62 -0
  129. package/src/openui/index.ts +20 -0
  130. package/src/openui/openui-artifact-renderer.tsx +542 -0
  131. package/src/primitives/artifact-pane.tsx +91 -0
  132. package/src/primitives/avatar.stories.tsx +95 -0
  133. package/src/primitives/avatar.tsx +47 -0
  134. package/src/primitives/badge.stories.tsx +57 -0
  135. package/src/primitives/badge.tsx +97 -0
  136. package/src/primitives/button.stories.tsx +48 -0
  137. package/src/primitives/button.tsx +115 -0
  138. package/src/primitives/card.stories.tsx +53 -0
  139. package/src/primitives/card.tsx +98 -0
  140. package/src/primitives/code-block.stories.tsx +115 -0
  141. package/src/primitives/code-block.tsx +22 -0
  142. package/src/primitives/design-tokens.stories.tsx +162 -0
  143. package/src/primitives/dialog.stories.tsx +176 -0
  144. package/src/primitives/dialog.tsx +137 -0
  145. package/src/primitives/drop-zone.stories.tsx +123 -0
  146. package/src/primitives/drop-zone.tsx +131 -0
  147. package/src/primitives/dropdown-menu.stories.tsx +122 -0
  148. package/src/primitives/dropdown-menu.tsx +214 -0
  149. package/src/primitives/empty-state.stories.tsx +81 -0
  150. package/src/primitives/empty-state.tsx +40 -0
  151. package/src/primitives/index.ts +118 -0
  152. package/src/primitives/input.stories.tsx +113 -0
  153. package/src/primitives/input.tsx +136 -0
  154. package/src/primitives/label.stories.tsx +84 -0
  155. package/src/primitives/label.tsx +24 -0
  156. package/src/primitives/progress.stories.tsx +93 -0
  157. package/src/primitives/progress.tsx +50 -0
  158. package/src/primitives/segmented-control.test.tsx +328 -0
  159. package/src/primitives/segmented-control.tsx +154 -0
  160. package/src/primitives/select.stories.tsx +164 -0
  161. package/src/primitives/select.tsx +158 -0
  162. package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
  163. package/src/primitives/sidebar-drop-zone.tsx +149 -0
  164. package/src/primitives/skeleton.stories.tsx +79 -0
  165. package/src/primitives/skeleton.tsx +55 -0
  166. package/src/primitives/stat-card.stories.tsx +137 -0
  167. package/src/primitives/stat-card.tsx +97 -0
  168. package/src/primitives/switch.stories.tsx +85 -0
  169. package/src/primitives/switch.tsx +28 -0
  170. package/src/primitives/table.stories.tsx +170 -0
  171. package/src/primitives/table.tsx +116 -0
  172. package/src/primitives/tabs.stories.tsx +180 -0
  173. package/src/primitives/tabs.tsx +71 -0
  174. package/src/primitives/terminal-display.stories.tsx +191 -0
  175. package/src/primitives/terminal-display.tsx +189 -0
  176. package/src/primitives/theme-toggle.stories.tsx +32 -0
  177. package/src/primitives/theme-toggle.tsx +96 -0
  178. package/src/primitives/toast.stories.tsx +155 -0
  179. package/src/primitives/toast.tsx +190 -0
  180. package/src/primitives/upload-progress.stories.tsx +120 -0
  181. package/src/primitives/upload-progress.tsx +110 -0
  182. package/src/run/expanded-tool-detail.stories.tsx +182 -0
  183. package/src/run/expanded-tool-detail.tsx +186 -0
  184. package/src/run/index.ts +13 -0
  185. package/src/run/inline-thinking-item.stories.tsx +136 -0
  186. package/src/run/inline-thinking-item.tsx +120 -0
  187. package/src/run/inline-tool-item.stories.tsx +222 -0
  188. package/src/run/inline-tool-item.tsx +190 -0
  189. package/src/run/run-group.stories.tsx +322 -0
  190. package/src/run/run-group.tsx +569 -0
  191. package/src/run/run-item-primitives.tsx +17 -0
  192. package/src/run/tool-call-feed.stories.tsx +294 -0
  193. package/src/run/tool-call-feed.tsx +192 -0
  194. package/src/run/tool-call-step.stories.tsx +198 -0
  195. package/src/run/tool-call-step.tsx +240 -0
  196. package/src/sdk-hooks.ts +38 -0
  197. package/src/stores/active-sessions-store.ts +455 -0
  198. package/src/stores/chat-store.ts +43 -0
  199. package/src/stores/index.ts +2 -0
  200. package/src/tool-previews/command-preview.tsx +116 -0
  201. package/src/tool-previews/diff-preview.tsx +85 -0
  202. package/src/tool-previews/glob-results-preview.tsx +98 -0
  203. package/src/tool-previews/grep-results-preview.tsx +157 -0
  204. package/src/tool-previews/index.ts +22 -0
  205. package/src/tool-previews/preview-primitives.tsx +84 -0
  206. package/src/tool-previews/question-preview.tsx +101 -0
  207. package/src/tool-previews/web-search-preview.tsx +117 -0
  208. package/src/tool-previews/write-file-preview.tsx +80 -0
  209. package/src/types/branding.ts +11 -0
  210. package/src/types/index.ts +5 -0
  211. package/src/types/message.ts +13 -0
  212. package/src/types/parts.ts +51 -0
  213. package/src/types/run.ts +56 -0
  214. package/src/types/tool-display.ts +41 -0
  215. package/src/utils/copy-text.ts +30 -0
  216. package/src/utils/format.test.ts +43 -0
  217. package/src/utils/format.ts +56 -0
  218. package/src/utils/index.ts +10 -0
  219. package/src/utils/time-ago.ts +9 -0
  220. 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
+ }