@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +43 -0
  3. package/dist/chat.d.ts +18 -8
  4. package/dist/chat.js +9 -7
  5. package/dist/chunk-4CLN43XT.js +45 -0
  6. package/dist/{chunk-AZWDI2JG.js → chunk-565EATZZ.js} +1 -1
  7. package/dist/{chunk-XROQW5J7.js → chunk-BWOUSEHM.js} +1 -1
  8. package/dist/{chunk-J2G7KCYH.js → chunk-KNYSKNZX.js} +113 -69
  9. package/dist/chunk-L57WRIND.js +59 -0
  10. package/dist/{chunk-ULDNFLIM.js → chunk-ND3ZFXQK.js} +11 -58
  11. package/dist/{chunk-AAUNOHVL.js → chunk-NWECTSOH.js} +1 -1
  12. package/dist/{chunk-R5GVQDL3.js → chunk-OXZRCXJK.js} +9 -7
  13. package/dist/{chunk-XPYPZMYT.js → chunk-Q7ZR6LSD.js} +1 -1
  14. package/dist/{chunk-RKC3GXHE.js → chunk-X53RZSX6.js} +99 -104
  15. package/dist/{chunk-PLWRAS5Z.js → chunk-XGOWUDDW.js} +270 -106
  16. package/dist/{chunk-ZRVH3WCA.js → chunk-XU3HB5QL.js} +21 -1
  17. package/dist/chunk-YTO5V6QS.js +19 -0
  18. package/dist/files.d.ts +41 -12
  19. package/dist/files.js +13 -5
  20. package/dist/hooks.js +9 -7
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +37 -24
  23. package/dist/primitives.d.ts +15 -1
  24. package/dist/primitives.js +5 -1
  25. package/dist/run.d.ts +22 -17
  26. package/dist/run.js +11 -8
  27. package/dist/sdk-hooks.js +9 -7
  28. package/dist/tool-previews.js +2 -2
  29. package/dist/utils.d.ts +6 -0
  30. package/dist/utils.js +6 -4
  31. package/package.json +1 -1
  32. package/src/chat/agent-timeline.stories.tsx +163 -10
  33. package/src/chat/agent-timeline.test.tsx +114 -7
  34. package/src/chat/agent-timeline.tsx +120 -158
  35. package/src/chat/chat-container.test.tsx +59 -0
  36. package/src/chat/user-message.test.tsx +33 -0
  37. package/src/chat/user-message.tsx +13 -8
  38. package/src/files/file-artifact-pane.stories.tsx +149 -0
  39. package/src/files/file-artifact-pane.test.tsx +192 -0
  40. package/src/files/file-artifact-pane.tsx +6 -1
  41. package/src/files/file-format.ts +42 -4
  42. package/src/files/file-preview-kind.test.ts +109 -0
  43. package/src/files/file-preview.tsx +315 -132
  44. package/src/files/index.ts +8 -1
  45. package/src/primitives/index.ts +3 -0
  46. package/src/primitives/text-shimmer.test.tsx +34 -0
  47. package/src/primitives/text-shimmer.tsx +82 -0
  48. package/src/run/inline-thinking-item.stories.tsx +2 -2
  49. package/src/run/inline-thinking-item.tsx +2 -2
  50. package/src/run/inline-tool-item.test.tsx +76 -0
  51. package/src/run/inline-tool-item.tsx +9 -7
  52. package/src/run/run-group.tsx +4 -3
  53. package/src/run/run-row-shell.test.tsx +74 -4
  54. package/src/run/run-row-shell.tsx +81 -53
  55. package/src/run/tool-call-step.tsx +9 -7
  56. package/src/utils/tool-display.test.ts +53 -0
  57. package/src/utils/tool-display.ts +16 -15
  58. package/dist/chunk-LQS34IGP.js +0 -0
  59. /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 **/*.ts in src/',
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 src/types.ts',
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 src/types.ts',
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: 'npx tsc --noEmit',
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 **/*.ts in src/',
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 src/routes/users.ts',
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 src/routes/sessions.ts',
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 src/routes/health.ts',
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: 'pnpm test',
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 openapi.yaml',
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 src/batch-writer.ts",
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 label and clean detail", () => {
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 src/batch-writer.ts")).toBeInTheDocument()
49
- expect(screen.getByText("src/batch-writer.ts")).toBeInTheDocument()
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 src/batch-writer.ts")).toBeInTheDocument()
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 src/batch-writer.ts",
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 src/lib/jitter.ts",
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 = {