@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 +6 -0
- package/dist/chat.js +1 -1
- package/dist/{chunk-OTZWQJ5W.js → chunk-IH2VRPGZ.js} +30 -14
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/chat/chat-container.test.tsx +62 -0
- package/src/chat/chat-container.tsx +20 -8
- package/src/chat/thinking-indicator.stories.tsx +20 -0
- package/src/chat/thinking-indicator.tsx +5 -1
package/CHANGELOG.md
CHANGED
package/dist/chat.js
CHANGED
|
@@ -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(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
(
|
|
352
|
-
item
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
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" }} />
|