@tangle-network/ui 11.6.0 → 11.8.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/README.md +43 -0
- package/dist/chat.d.ts +18 -8
- package/dist/chat.js +9 -7
- package/dist/chunk-4CLN43XT.js +45 -0
- package/dist/{chunk-AZWDI2JG.js → chunk-565EATZZ.js} +1 -1
- package/dist/{chunk-XROQW5J7.js → chunk-BWOUSEHM.js} +1 -1
- package/dist/{chunk-J2G7KCYH.js → chunk-KNYSKNZX.js} +113 -69
- package/dist/chunk-L57WRIND.js +59 -0
- package/dist/{chunk-ULDNFLIM.js → chunk-ND3ZFXQK.js} +11 -58
- package/dist/{chunk-AAUNOHVL.js → chunk-NWECTSOH.js} +1 -1
- package/dist/{chunk-R5GVQDL3.js → chunk-OXZRCXJK.js} +9 -7
- package/dist/{chunk-XPYPZMYT.js → chunk-Q7ZR6LSD.js} +1 -1
- package/dist/{chunk-RKC3GXHE.js → chunk-X53RZSX6.js} +99 -104
- package/dist/{chunk-PLWRAS5Z.js → chunk-XGOWUDDW.js} +270 -106
- package/dist/{chunk-ZRVH3WCA.js → chunk-XU3HB5QL.js} +21 -1
- package/dist/chunk-YTO5V6QS.js +19 -0
- package/dist/files.d.ts +41 -12
- package/dist/files.js +13 -5
- package/dist/hooks.js +9 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -24
- package/dist/primitives.d.ts +15 -1
- package/dist/primitives.js +5 -1
- package/dist/run.d.ts +22 -17
- package/dist/run.js +11 -8
- package/dist/sdk-hooks.js +9 -7
- package/dist/tool-previews.js +2 -2
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +6 -4
- package/package.json +1 -1
- package/src/chat/agent-timeline.stories.tsx +163 -10
- package/src/chat/agent-timeline.test.tsx +114 -7
- package/src/chat/agent-timeline.tsx +120 -158
- package/src/chat/chat-container.test.tsx +59 -0
- package/src/chat/user-message.test.tsx +33 -0
- package/src/chat/user-message.tsx +13 -8
- package/src/files/file-artifact-pane.stories.tsx +149 -0
- package/src/files/file-artifact-pane.test.tsx +192 -0
- package/src/files/file-artifact-pane.tsx +6 -1
- package/src/files/file-format.ts +42 -4
- package/src/files/file-preview-kind.test.ts +109 -0
- package/src/files/file-preview.tsx +315 -132
- package/src/files/index.ts +8 -1
- package/src/primitives/index.ts +3 -0
- package/src/primitives/text-shimmer.test.tsx +34 -0
- package/src/primitives/text-shimmer.tsx +82 -0
- package/src/run/inline-thinking-item.stories.tsx +2 -2
- package/src/run/inline-thinking-item.tsx +2 -2
- package/src/run/inline-tool-item.test.tsx +76 -0
- package/src/run/inline-tool-item.tsx +9 -7
- package/src/run/run-group.tsx +4 -3
- package/src/run/run-row-shell.test.tsx +74 -4
- package/src/run/run-row-shell.tsx +81 -53
- package/src/run/tool-call-step.tsx +9 -7
- package/src/utils/tool-display.test.ts +53 -0
- package/src/utils/tool-display.ts +16 -15
- package/dist/chunk-LQS34IGP.js +0 -0
- /package/dist/{chunk-EJ5NBW7V.js → chunk-LDLOJKPC.js} +0 -0
|
@@ -3,6 +3,9 @@ import { useState } from 'react'
|
|
|
3
3
|
import { Code, FileText } from 'lucide-react'
|
|
4
4
|
import { AgentTimeline } from './agent-timeline'
|
|
5
5
|
import type { AgentTimelineItem } from './agent-timeline'
|
|
6
|
+
import { ChatContainer } from './chat-container'
|
|
7
|
+
import type { SessionMessage } from '../types/message'
|
|
8
|
+
import type { SessionPart } from '../types/parts'
|
|
6
9
|
|
|
7
10
|
const meta: Meta<typeof AgentTimeline> = {
|
|
8
11
|
title: 'Chat/AgentTimeline',
|
|
@@ -52,8 +55,9 @@ const codingSession: AgentTimelineItem[] = [
|
|
|
52
55
|
call: {
|
|
53
56
|
id: 'tc-1',
|
|
54
57
|
type: 'glob',
|
|
55
|
-
label: 'Find
|
|
58
|
+
label: 'Find',
|
|
56
59
|
status: 'success',
|
|
60
|
+
detail: 'src/**/*.ts',
|
|
57
61
|
output:
|
|
58
62
|
'src/index.ts\nsrc/types.ts\nsrc/utils/format.ts\nsrc/utils/parse.ts',
|
|
59
63
|
duration: 18,
|
|
@@ -65,8 +69,9 @@ const codingSession: AgentTimelineItem[] = [
|
|
|
65
69
|
call: {
|
|
66
70
|
id: 'tc-2',
|
|
67
71
|
type: 'read',
|
|
68
|
-
label: 'Read
|
|
72
|
+
label: 'Read',
|
|
69
73
|
status: 'success',
|
|
74
|
+
detail: 'src/types.ts',
|
|
70
75
|
output:
|
|
71
76
|
'// Existing types\nexport type Maybe<T> = T | null | undefined\nexport type Nullable<T> = T | null',
|
|
72
77
|
duration: 31,
|
|
@@ -86,8 +91,9 @@ const codingSession: AgentTimelineItem[] = [
|
|
|
86
91
|
call: {
|
|
87
92
|
id: 'tc-3',
|
|
88
93
|
type: 'write',
|
|
89
|
-
label: 'Write
|
|
94
|
+
label: 'Write',
|
|
90
95
|
status: 'success',
|
|
96
|
+
detail: 'src/types.ts',
|
|
91
97
|
output: 'File written — 47 lines added.',
|
|
92
98
|
duration: 22,
|
|
93
99
|
},
|
|
@@ -98,7 +104,7 @@ const codingSession: AgentTimelineItem[] = [
|
|
|
98
104
|
call: {
|
|
99
105
|
id: 'tc-4',
|
|
100
106
|
type: 'bash',
|
|
101
|
-
label: '
|
|
107
|
+
label: 'Shell',
|
|
102
108
|
status: 'success',
|
|
103
109
|
detail: 'npx tsc --noEmit --strict',
|
|
104
110
|
output: '✓ No type errors',
|
|
@@ -201,8 +207,9 @@ export const WhileThinking: Story = {
|
|
|
201
207
|
call: {
|
|
202
208
|
id: 'tc-1',
|
|
203
209
|
type: 'glob',
|
|
204
|
-
label: 'Find
|
|
210
|
+
label: 'Find',
|
|
205
211
|
status: 'success',
|
|
212
|
+
detail: 'src/**/*.ts',
|
|
206
213
|
duration: 14,
|
|
207
214
|
},
|
|
208
215
|
},
|
|
@@ -234,24 +241,27 @@ export const WithToolGroup: Story = {
|
|
|
234
241
|
{
|
|
235
242
|
id: 'tc-1',
|
|
236
243
|
type: 'read',
|
|
237
|
-
label: 'Read
|
|
244
|
+
label: 'Read',
|
|
238
245
|
status: 'success',
|
|
246
|
+
detail: 'src/routes/users.ts',
|
|
239
247
|
output: "router.get('/', authMiddleware, listUsers)\nrouter.post('/', authMiddleware, createUser)",
|
|
240
248
|
duration: 24,
|
|
241
249
|
},
|
|
242
250
|
{
|
|
243
251
|
id: 'tc-2',
|
|
244
252
|
type: 'read',
|
|
245
|
-
label: 'Read
|
|
253
|
+
label: 'Read',
|
|
246
254
|
status: 'success',
|
|
255
|
+
detail: 'src/routes/sessions.ts',
|
|
247
256
|
output: "router.get('/', listSessions) // missing auth!\nrouter.delete('/:id', authMiddleware, deleteSession)",
|
|
248
257
|
duration: 19,
|
|
249
258
|
},
|
|
250
259
|
{
|
|
251
260
|
id: 'tc-3',
|
|
252
261
|
type: 'read',
|
|
253
|
-
label: 'Read
|
|
262
|
+
label: 'Read',
|
|
254
263
|
status: 'success',
|
|
264
|
+
detail: 'src/routes/health.ts',
|
|
255
265
|
output: "router.get('/', healthCheck) // intentionally public",
|
|
256
266
|
duration: 11,
|
|
257
267
|
},
|
|
@@ -305,7 +315,7 @@ export const WithError: Story = {
|
|
|
305
315
|
call: {
|
|
306
316
|
id: 'tc-1',
|
|
307
317
|
type: 'bash',
|
|
308
|
-
label: '
|
|
318
|
+
label: 'Shell',
|
|
309
319
|
status: 'error',
|
|
310
320
|
detail: 'pnpm test --run',
|
|
311
321
|
output:
|
|
@@ -351,8 +361,9 @@ export const WithArtifact: Story = {
|
|
|
351
361
|
call: {
|
|
352
362
|
id: 'tc-1',
|
|
353
363
|
type: 'write',
|
|
354
|
-
label: 'Write
|
|
364
|
+
label: 'Write',
|
|
355
365
|
status: 'success',
|
|
366
|
+
detail: 'openapi.yaml',
|
|
356
367
|
output: '187 lines written.',
|
|
357
368
|
duration: 44,
|
|
358
369
|
},
|
|
@@ -438,3 +449,145 @@ export const StatusTones: Story = {
|
|
|
438
449
|
],
|
|
439
450
|
},
|
|
440
451
|
}
|
|
452
|
+
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// Full transcript through ChatContainer — the real consumer path. Tool titles
|
|
455
|
+
// and descriptions come from `getToolDisplayMetadata`, so this story shows the
|
|
456
|
+
// row grammar a product sees (not hand-written labels): prose, reasoning, a
|
|
457
|
+
// tool sequence, a running row, an error row, and a streaming reply.
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
const transcriptMessages: SessionMessage[] = [
|
|
461
|
+
{ id: 'u-1', role: 'user', time: { created: NOW - 240_000 } },
|
|
462
|
+
{ id: 'a-1', role: 'assistant', time: { created: NOW - 230_000 } },
|
|
463
|
+
{ id: 'u-2', role: 'user', time: { created: NOW - 60_000 } },
|
|
464
|
+
{ id: 'a-2', role: 'assistant', time: { created: NOW - 50_000 } },
|
|
465
|
+
]
|
|
466
|
+
|
|
467
|
+
const transcriptParts: Record<string, SessionPart[]> = {
|
|
468
|
+
'u-1': [
|
|
469
|
+
{
|
|
470
|
+
type: 'text',
|
|
471
|
+
text: 'The retry loop in `src/batch-writer.ts` hammers the API when the token expires. Add jittered backoff and make sure the tests cover it.',
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
'a-1': [
|
|
475
|
+
{
|
|
476
|
+
type: 'reasoning',
|
|
477
|
+
text: 'The loop retries immediately on 401. I should read the writer, find where the delay is computed, add exponential backoff with jitter, and extend the existing test.',
|
|
478
|
+
time: { start: NOW - 229_000, end: NOW - 226_000 },
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
type: 'tool',
|
|
482
|
+
id: 't-1',
|
|
483
|
+
tool: 'read',
|
|
484
|
+
state: {
|
|
485
|
+
status: 'completed',
|
|
486
|
+
input: { file_path: 'src/batch-writer.ts' },
|
|
487
|
+
output: 'export async function flush(batch: Row[]) {\n for (let attempt = 0; attempt < 5; attempt++) {\n try { return await post(batch) } catch (err) { /* retry */ }\n }\n}',
|
|
488
|
+
time: { start: NOW - 225_000, end: NOW - 224_960 },
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
type: 'tool',
|
|
493
|
+
id: 't-2',
|
|
494
|
+
tool: 'grep',
|
|
495
|
+
state: {
|
|
496
|
+
status: 'completed',
|
|
497
|
+
input: { pattern: 'flush\\(', path: 'src/' },
|
|
498
|
+
output: 'src/batch-writer.ts:12\nsrc/batch-writer.test.ts:8\nsrc/batch-writer.test.ts:31',
|
|
499
|
+
time: { start: NOW - 224_000, end: NOW - 223_970 },
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
type: 'tool',
|
|
504
|
+
id: 't-3',
|
|
505
|
+
tool: 'edit',
|
|
506
|
+
state: {
|
|
507
|
+
status: 'completed',
|
|
508
|
+
input: {
|
|
509
|
+
file_path: 'src/batch-writer.ts',
|
|
510
|
+
old_string: '} catch (err) { /* retry */ }',
|
|
511
|
+
new_string: '} catch (err) {\n await sleep(backoff(attempt))\n }',
|
|
512
|
+
},
|
|
513
|
+
output: 'Applied 1 edit',
|
|
514
|
+
time: { start: NOW - 223_000, end: NOW - 222_980 },
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
type: 'tool',
|
|
519
|
+
id: 't-4',
|
|
520
|
+
tool: 'bash',
|
|
521
|
+
state: {
|
|
522
|
+
status: 'completed',
|
|
523
|
+
input: { command: 'pnpm vitest run src/batch-writer.test.ts' },
|
|
524
|
+
output: ' ✓ src/batch-writer.test.ts (3)\n\nTest Files 1 passed (1)\n Tests 3 passed (3)',
|
|
525
|
+
time: { start: NOW - 222_000, end: NOW - 219_600 },
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
type: 'text',
|
|
530
|
+
text: 'Added `backoff(attempt)` — exponential from 200ms with ±25% jitter, capped at 5s — and the writer now waits before each retry. The existing three tests still pass.',
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
'u-2': [
|
|
534
|
+
{
|
|
535
|
+
type: 'text',
|
|
536
|
+
text: 'Now add a test that proves the jitter actually spreads the retries.',
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
'a-2': [
|
|
540
|
+
{
|
|
541
|
+
type: 'tool',
|
|
542
|
+
id: 't-5',
|
|
543
|
+
tool: 'write',
|
|
544
|
+
state: {
|
|
545
|
+
status: 'completed',
|
|
546
|
+
input: {
|
|
547
|
+
file_path: 'src/batch-writer.jitter.test.ts',
|
|
548
|
+
content: "import { backoff } from './batch-writer'\n\ntest('jitter spreads retries', () => {\n const delays = new Set(Array.from({ length: 50 }, () => backoff(2)))\n expect(delays.size).toBeGreaterThan(10)\n})",
|
|
549
|
+
},
|
|
550
|
+
output: 'File written',
|
|
551
|
+
time: { start: NOW - 49_000, end: NOW - 48_970 },
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
type: 'tool',
|
|
556
|
+
id: 't-6',
|
|
557
|
+
tool: 'bash',
|
|
558
|
+
state: {
|
|
559
|
+
status: 'error',
|
|
560
|
+
input: { command: 'pnpm vitest run src/batch-writer.jitter.test.ts' },
|
|
561
|
+
error: "Cannot find module './batch-writer' — did you mean './batch-writer.ts'? (resolve.extensions does not include .ts in this config)",
|
|
562
|
+
time: { start: NOW - 48_000, end: NOW - 46_800 },
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
type: 'tool',
|
|
567
|
+
id: 't-7',
|
|
568
|
+
tool: 'bash',
|
|
569
|
+
state: {
|
|
570
|
+
status: 'running',
|
|
571
|
+
input: { command: 'pnpm vitest run src/batch-writer.jitter.test.ts --config vitest.config.ts' },
|
|
572
|
+
time: { start: NOW - 3_000 },
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
type: 'text',
|
|
577
|
+
text: 'The first run failed on module resolution — the test runner was picking up the root config. Re-running with the package config',
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export const FullTranscript: Story = {
|
|
583
|
+
render: () => (
|
|
584
|
+
<div className="h-screen">
|
|
585
|
+
<ChatContainer
|
|
586
|
+
presentation="timeline"
|
|
587
|
+
messages={transcriptMessages}
|
|
588
|
+
partMap={transcriptParts}
|
|
589
|
+
isStreaming
|
|
590
|
+
/>
|
|
591
|
+
</div>
|
|
592
|
+
),
|
|
593
|
+
}
|
|
@@ -33,7 +33,7 @@ function toolItem(part: ToolPart): AgentTimelineItem {
|
|
|
33
33
|
call: {
|
|
34
34
|
id: part.id,
|
|
35
35
|
type: "edit",
|
|
36
|
-
label: "Edit
|
|
36
|
+
label: "Edit",
|
|
37
37
|
status: "success",
|
|
38
38
|
detail: "src/batch-writer.ts",
|
|
39
39
|
output: "Applied 1 edit",
|
|
@@ -43,10 +43,10 @@ function toolItem(part: ToolPart): AgentTimelineItem {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
describe("AgentTimeline tool actions", () => {
|
|
46
|
-
it("renders a tool call's
|
|
46
|
+
it("renders a tool call's verb and its path, once each", () => {
|
|
47
47
|
render(<AgentTimeline items={[toolItem(editPart)]} />)
|
|
48
|
-
expect(screen.getByText("Edit
|
|
49
|
-
expect(screen.
|
|
48
|
+
expect(screen.getByText("Edit")).toBeInTheDocument()
|
|
49
|
+
expect(screen.getAllByText("src/batch-writer.ts")).toHaveLength(1)
|
|
50
50
|
})
|
|
51
51
|
|
|
52
52
|
it("renders renderToolActions beside the tool item, called with the source part", () => {
|
|
@@ -81,7 +81,7 @@ describe("AgentTimeline tool actions", () => {
|
|
|
81
81
|
|
|
82
82
|
it("renders tool items unchanged when no renderToolActions is provided", () => {
|
|
83
83
|
render(<AgentTimeline items={[toolItem(editPart)]} />)
|
|
84
|
-
expect(screen.getByText("Edit
|
|
84
|
+
expect(screen.getByText("Edit")).toBeInTheDocument()
|
|
85
85
|
expect(screen.queryByRole("button", { name: /open/i })).not.toBeInTheDocument()
|
|
86
86
|
})
|
|
87
87
|
|
|
@@ -97,14 +97,14 @@ describe("AgentTimeline tool actions", () => {
|
|
|
97
97
|
{
|
|
98
98
|
id: "tool-1",
|
|
99
99
|
type: "edit",
|
|
100
|
-
label: "Edit
|
|
100
|
+
label: "Edit",
|
|
101
101
|
status: "success",
|
|
102
102
|
detail: "src/batch-writer.ts",
|
|
103
103
|
},
|
|
104
104
|
{
|
|
105
105
|
id: "tool-2",
|
|
106
106
|
type: "read",
|
|
107
|
-
label: "Read
|
|
107
|
+
label: "Read",
|
|
108
108
|
status: "success",
|
|
109
109
|
detail: "src/lib/jitter.ts",
|
|
110
110
|
},
|
|
@@ -160,6 +160,113 @@ describe("AgentTimeline tool actions", () => {
|
|
|
160
160
|
expect(screen.getByRole("button", { name: /show less/i })).toBeInTheDocument()
|
|
161
161
|
})
|
|
162
162
|
|
|
163
|
+
it("names a tool group for assistive tech but draws no header above it", () => {
|
|
164
|
+
const groupItem: AgentTimelineItem = {
|
|
165
|
+
id: "group-1",
|
|
166
|
+
kind: "tool_group",
|
|
167
|
+
title: "Tool activity",
|
|
168
|
+
calls: [
|
|
169
|
+
{ id: "tool-1", type: "edit", label: "Edit", status: "success", detail: "src/a.ts" },
|
|
170
|
+
{ id: "tool-2", type: "read", label: "Read", status: "success", detail: "src/b.ts" },
|
|
171
|
+
],
|
|
172
|
+
parts: [editPart, readPart],
|
|
173
|
+
}
|
|
174
|
+
render(<AgentTimeline items={[groupItem]} />)
|
|
175
|
+
expect(screen.queryByText("Tool activity")).not.toBeInTheDocument()
|
|
176
|
+
expect(screen.getByRole("group", { name: "Tool activity" })).toBeInTheDocument()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it("renders assistant prose without a timestamp header", () => {
|
|
180
|
+
const stamp = new Date(2026, 0, 1, 15, 42)
|
|
181
|
+
render(
|
|
182
|
+
<AgentTimeline
|
|
183
|
+
items={[
|
|
184
|
+
{ id: "a-1", kind: "message", role: "assistant", content: "Done.", timestamp: stamp },
|
|
185
|
+
]}
|
|
186
|
+
/>,
|
|
187
|
+
)
|
|
188
|
+
expect(screen.getByText("Done.")).toBeInTheDocument()
|
|
189
|
+
expect(
|
|
190
|
+
screen.queryByText(
|
|
191
|
+
stamp.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }),
|
|
192
|
+
),
|
|
193
|
+
).not.toBeInTheDocument()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("keeps the user bubble's timestamp", () => {
|
|
197
|
+
const stamp = new Date(2026, 0, 1, 15, 42)
|
|
198
|
+
render(
|
|
199
|
+
<AgentTimeline
|
|
200
|
+
items={[{ id: "u-1", kind: "message", role: "user", content: "Hi", timestamp: stamp }]}
|
|
201
|
+
/>,
|
|
202
|
+
)
|
|
203
|
+
expect(
|
|
204
|
+
screen.getByText(
|
|
205
|
+
stamp.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }),
|
|
206
|
+
),
|
|
207
|
+
).toBeInTheDocument()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it("lays rows out in a plain column — no spine gutter, no connector", () => {
|
|
211
|
+
const { container } = render(
|
|
212
|
+
<AgentTimeline
|
|
213
|
+
items={[
|
|
214
|
+
{ id: "a-1", kind: "message", role: "assistant", content: "Reading." },
|
|
215
|
+
toolItem(editPart),
|
|
216
|
+
{ id: "s-1", kind: "status", label: "Type-checks clean", tone: "success" },
|
|
217
|
+
]}
|
|
218
|
+
/>,
|
|
219
|
+
)
|
|
220
|
+
expect(container.querySelector('[class*="grid-cols-"]')).toBeNull()
|
|
221
|
+
expect(container.querySelector(".ring-4")).toBeNull()
|
|
222
|
+
const steps = container.querySelectorAll("[data-timeline-step]")
|
|
223
|
+
expect(Array.from(steps).map((el) => el.getAttribute("data-timeline-step"))).toEqual([
|
|
224
|
+
"prose",
|
|
225
|
+
"tool",
|
|
226
|
+
"card",
|
|
227
|
+
])
|
|
228
|
+
// Tool rows bleed past the prose edge so the glyph sits on it.
|
|
229
|
+
expect(steps[1].className).toContain("-mx-3")
|
|
230
|
+
expect(steps[0].className).not.toContain("-mx-3")
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it("spaces rows by their neighbours: turns widest, tool sequences tightest", () => {
|
|
234
|
+
const tool2 = { ...toolItem(readPart), id: "item-2" }
|
|
235
|
+
const { container } = render(
|
|
236
|
+
<AgentTimeline
|
|
237
|
+
items={[
|
|
238
|
+
{ id: "u-1", kind: "message", role: "user", content: "Go" },
|
|
239
|
+
toolItem(editPart),
|
|
240
|
+
tool2,
|
|
241
|
+
{ id: "a-1", kind: "message", role: "assistant", content: "Done." },
|
|
242
|
+
{ id: "u-2", kind: "message", role: "user", content: "Thanks" },
|
|
243
|
+
]}
|
|
244
|
+
/>,
|
|
245
|
+
)
|
|
246
|
+
const classes = Array.from(container.querySelectorAll("[data-timeline-step]")).map(
|
|
247
|
+
(el) => el.className,
|
|
248
|
+
)
|
|
249
|
+
expect(classes[0]).not.toMatch(/mt-\d/)
|
|
250
|
+
expect(classes[1]).toContain("mt-4") // user → tool: turn boundary
|
|
251
|
+
expect(classes[2]).toContain("mt-1") // tool → tool
|
|
252
|
+
expect(classes[3]).toContain("mt-1") // tool → prose
|
|
253
|
+
expect(classes[4]).toContain("mt-4") // prose → user
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it("softens the streaming cursor to a muted caret", () => {
|
|
257
|
+
const { container } = render(
|
|
258
|
+
<AgentTimeline
|
|
259
|
+
items={[
|
|
260
|
+
{ id: "a-1", kind: "message", role: "assistant", content: "Typing", isStreaming: true },
|
|
261
|
+
]}
|
|
262
|
+
/>,
|
|
263
|
+
)
|
|
264
|
+
const caret = container.querySelector("[data-streaming-caret]")
|
|
265
|
+
expect(caret).not.toBeNull()
|
|
266
|
+
expect(caret!.className).toContain("bg-muted-foreground")
|
|
267
|
+
expect(caret!.className).not.toContain("bg-primary")
|
|
268
|
+
})
|
|
269
|
+
|
|
163
270
|
it("renders the source part's real input in the expanded detail", async () => {
|
|
164
271
|
const user = userEvent.setup()
|
|
165
272
|
const probePart: ToolPart = {
|