@tangle-network/ui 11.2.2 → 11.2.3

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,11 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 7eeed23: Show the animated thinking row immediately after a user submits a message, before the first assistant event arrives.
8
+
3
9
  ## 11.2.2
4
10
 
5
11
  ### Patch Changes
package/dist/chat.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  MessageList,
6
6
  ThinkingIndicator,
7
7
  UserMessage
8
- } from "./chunk-OTZWQJ5W.js";
8
+ } from "./chunk-IH2VRPGZ.js";
9
9
  import "./chunk-AZWDI2JG.js";
10
10
  import "./chunk-SRAKCVPO.js";
11
11
  import "./chunk-4ORVXSMU.js";
@@ -121,17 +121,25 @@ function ThinkingIndicator({ className }) {
121
121
  const interval = window.setInterval(() => setElapsed((current) => current + 1), 1e3);
122
122
  return () => window.clearInterval(interval);
123
123
  }, []);
124
- return /* @__PURE__ */ jsxs2("div", { className: cn("flex items-center gap-2 px-3 py-1.5", className), children: [
125
- /* @__PURE__ */ jsxs2("div", { className: "flex gap-[3px]", children: [
126
- /* @__PURE__ */ jsx3("span", { className: "h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]", style: { animationDelay: "0ms" } }),
127
- /* @__PURE__ */ jsx3("span", { className: "h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]", style: { animationDelay: "150ms" } }),
128
- /* @__PURE__ */ jsx3("span", { className: "h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]", style: { animationDelay: "300ms" } })
129
- ] }),
130
- elapsed > 3 && /* @__PURE__ */ jsxs2("span", { className: "text-[var(--font-size-xs)] tabular-nums text-muted-foreground", children: [
131
- elapsed,
132
- "s"
133
- ] })
134
- ] });
124
+ return /* @__PURE__ */ jsxs2(
125
+ "div",
126
+ {
127
+ role: "status",
128
+ "aria-label": "Agent is thinking",
129
+ className: cn("flex items-center gap-2 px-3 py-1.5", className),
130
+ children: [
131
+ /* @__PURE__ */ jsxs2("div", { className: "flex gap-[3px]", children: [
132
+ /* @__PURE__ */ jsx3("span", { className: "h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]", style: { animationDelay: "0ms" } }),
133
+ /* @__PURE__ */ jsx3("span", { className: "h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]", style: { animationDelay: "150ms" } }),
134
+ /* @__PURE__ */ jsx3("span", { className: "h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]", style: { animationDelay: "300ms" } })
135
+ ] }),
136
+ elapsed > 3 && /* @__PURE__ */ jsxs2("span", { className: "text-[var(--font-size-xs)] tabular-nums text-muted-foreground", children: [
137
+ elapsed,
138
+ "s"
139
+ ] })
140
+ ]
141
+ }
142
+ );
135
143
  }
136
144
 
137
145
  // src/chat/agent-timeline.tsx
@@ -439,7 +447,15 @@ function mapToolPartToTimelineType(part) {
439
447
  }
440
448
  function buildTimelineItems(messages, partMap, isStreaming, onOpenUIAction, enableOpenUI = true) {
441
449
  const items = [];
442
- const lastAssistantMessage = [...messages].reverse().find((message) => message.role === "assistant");
450
+ let latestUserIndex = -1;
451
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
452
+ if (messages[index].role === "user") {
453
+ latestUserIndex = index;
454
+ break;
455
+ }
456
+ }
457
+ const responseMessages = latestUserIndex >= 0 ? messages.slice(latestUserIndex + 1) : messages;
458
+ const lastAssistantMessage = [...responseMessages].reverse().find((message) => message.role === "assistant");
443
459
  const toToolCall = (part) => {
444
460
  const meta = getToolDisplayMetadata(part);
445
461
  const start = part.state.time?.start;
@@ -566,9 +582,9 @@ function buildTimelineItems(messages, partMap, isStreaming, onOpenUIAction, enab
566
582
  });
567
583
  flushToolBuffer(parts.length);
568
584
  }
569
- const showThinking = isStreaming && lastAssistantMessage != null && !items.some(
585
+ const showThinking = isStreaming && latestUserIndex >= 0 && (lastAssistantMessage == null || !items.some(
570
586
  (item) => item.kind === "message" && item.role === "assistant" && item.id.startsWith(lastAssistantMessage.id)
571
- );
587
+ ));
572
588
  return { items, showThinking };
573
589
  }
574
590
  var ChatContainer = memo3(
package/dist/index.js CHANGED
@@ -137,7 +137,7 @@ import {
137
137
  MessageList,
138
138
  ThinkingIndicator,
139
139
  UserMessage
140
- } from "./chunk-OTZWQJ5W.js";
140
+ } from "./chunk-IH2VRPGZ.js";
141
141
  import {
142
142
  useAutoScroll,
143
143
  useRunCollapseState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.2.2",
3
+ "version": "11.2.3",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,6 +27,68 @@ const editPart: ToolPart = {
27
27
  const partMap: Record<string, SessionPart[]> = { m1: [editPart] }
28
28
 
29
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
+
30
92
  it("shows a clean tool detail (the file path), not the raw input JSON", () => {
31
93
  render(
32
94
  <ChatContainer
@@ -179,7 +179,18 @@ function buildTimelineItems(
179
179
  enableOpenUI = true,
180
180
  ): { items: AgentTimelineItem[]; showThinking: boolean } {
181
181
  const items: AgentTimelineItem[] = [];
182
- const lastAssistantMessage = [...messages].reverse().find((message) => message.role === "assistant");
182
+ let latestUserIndex = -1;
183
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
184
+ if (messages[index].role === "user") {
185
+ latestUserIndex = index;
186
+ break;
187
+ }
188
+ }
189
+ const responseMessages =
190
+ latestUserIndex >= 0 ? messages.slice(latestUserIndex + 1) : messages;
191
+ const lastAssistantMessage = [...responseMessages]
192
+ .reverse()
193
+ .find((message) => message.role === "assistant");
183
194
  const toToolCall = (part: ToolPart) => {
184
195
  const meta = getToolDisplayMetadata(part as ToolPart);
185
196
  const start = part.state.time?.start;
@@ -346,13 +357,14 @@ function buildTimelineItems(
346
357
 
347
358
  const showThinking =
348
359
  isStreaming &&
349
- lastAssistantMessage != null &&
350
- !items.some(
351
- (item) =>
352
- item.kind === "message" &&
353
- item.role === "assistant" &&
354
- item.id.startsWith(lastAssistantMessage.id),
355
- );
360
+ latestUserIndex >= 0 &&
361
+ (lastAssistantMessage == null ||
362
+ !items.some(
363
+ (item) =>
364
+ item.kind === "message" &&
365
+ item.role === "assistant" &&
366
+ item.id.startsWith(lastAssistantMessage.id),
367
+ ));
356
368
 
357
369
  return { items, showThinking };
358
370
  }
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react'
2
+ import { ChatContainer } from './chat-container'
2
3
  import { ThinkingIndicator } from './thinking-indicator'
3
4
 
4
5
  const meta: Meta<typeof ThinkingIndicator> = {
@@ -42,6 +43,25 @@ export const InContext: Story = {
42
43
  ),
43
44
  }
44
45
 
46
+ /**
47
+ * The production handoff state: the user's message is already visible while
48
+ * the first assistant event has not arrived yet.
49
+ */
50
+ export const PendingChatResponse: Story = {
51
+ render: () => (
52
+ <div className="h-80 rounded-xl border border-border bg-[var(--bg-root)]">
53
+ <ChatContainer
54
+ messages={[{ id: 'user-1', role: 'user' }]}
55
+ partMap={{
56
+ 'user-1': [{ type: 'text', text: 'Hello — can you inspect this sandbox?' }],
57
+ }}
58
+ isStreaming
59
+ presentation="timeline"
60
+ />
61
+ </div>
62
+ ),
63
+ }
64
+
45
65
  /**
46
66
  * Multiple indicators to show staggered animation timing.
47
67
  */
@@ -14,7 +14,11 @@ export function ThinkingIndicator({ className }: ThinkingIndicatorProps) {
14
14
  }, []);
15
15
 
16
16
  return (
17
- <div className={cn("flex items-center gap-2 px-3 py-1.5", className)}>
17
+ <div
18
+ role="status"
19
+ aria-label="Agent is thinking"
20
+ className={cn("flex items-center gap-2 px-3 py-1.5", className)}
21
+ >
18
22
  <div className="flex gap-[3px]">
19
23
  <span className="h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "0ms" }} />
20
24
  <span className="h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "150ms" }} />