@tangle-network/ui 7.0.0 → 8.1.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.
@@ -0,0 +1,115 @@
1
+ import { type ReactNode } from "react";
2
+ import * as Collapsible from "@radix-ui/react-collapsible";
3
+ import { ChevronDown, ChevronRight, Loader2, Sparkles } from "lucide-react";
4
+ import { cn } from "../lib/utils";
5
+
6
+ export interface AssistantRunShellProps {
7
+ /** Header label, e.g. the agent name or "Tools". */
8
+ label: string;
9
+ /** Terse stat line beside the label, e.g. "3 tools, 2s thinking". */
10
+ summary?: string;
11
+ /** One-line preview shown next to the label AND below the header when collapsed. */
12
+ collapsedPreview?: string;
13
+ /** Small trailing glyphs before the status pill (e.g. category badges). */
14
+ badges?: ReactNode;
15
+ /** Drives the status pill and header spinner. */
16
+ isStreaming?: boolean;
17
+ collapsed: boolean;
18
+ onToggle: () => void;
19
+ /** Actions rendered outside the collapse trigger, right of the header. */
20
+ headerActions?: ReactNode;
21
+ children: ReactNode;
22
+ className?: string;
23
+ }
24
+
25
+ /**
26
+ * The collapsible "assistant run" container shared by `RunGroup` (session-model
27
+ * driven) and `AgentTimeline` (declarative item list). Owns the header
28
+ * (label · summary · badges · status pill · chevron), the collapsed preview, and
29
+ * the Radix collapse — so both transcripts fold agent activity the same way and
30
+ * there is one implementation of a run, not two. It renders only chrome; callers
31
+ * pass the run body (tool rows, reasoning, text) as `children`.
32
+ */
33
+ export function AssistantRunShell({
34
+ label,
35
+ summary,
36
+ collapsedPreview,
37
+ badges,
38
+ isStreaming,
39
+ collapsed,
40
+ onToggle,
41
+ headerActions,
42
+ children,
43
+ className,
44
+ }: AssistantRunShellProps) {
45
+ return (
46
+ <Collapsible.Root open={!collapsed} onOpenChange={() => onToggle()}>
47
+ <div
48
+ className={cn(
49
+ "rounded-[28px] border border-[var(--border-subtle)] bg-[var(--bg-card)] shadow-none",
50
+ className,
51
+ )}
52
+ >
53
+ <div className="flex items-start gap-3 px-3 py-2.5">
54
+ <Collapsible.Trigger asChild>
55
+ <button
56
+ type="button"
57
+ className="w-full rounded-[20px] bg-transparent px-0 py-0 text-left transition-colors hover:bg-transparent"
58
+ >
59
+ <div className="flex items-center gap-2">
60
+ <span className="font-semibold text-foreground text-sm">{label}</span>
61
+
62
+ {summary ? (
63
+ <span className="text-[11px] text-muted-foreground">{summary}</span>
64
+ ) : null}
65
+ {collapsed && collapsedPreview ? (
66
+ <span className="min-w-0 truncate text-[11px] text-foreground/70">
67
+ {collapsedPreview}
68
+ </span>
69
+ ) : null}
70
+
71
+ <div className="ml-auto flex shrink-0 items-center gap-1.5">
72
+ {badges}
73
+
74
+ {isStreaming ? (
75
+ <span className="inline-flex items-center gap-1 rounded-full border border-[var(--border-accent)] bg-[var(--accent-surface-soft)] px-2 py-px text-[10px] font-semibold uppercase text-[var(--accent-text)]">
76
+ <Loader2 className="h-2.5 w-2.5 animate-spin" />
77
+ Running
78
+ </span>
79
+ ) : (
80
+ <span className="inline-flex items-center gap-1 rounded-full border border-border px-2 py-px text-[10px] font-semibold uppercase text-muted-foreground">
81
+ <Sparkles className="h-2.5 w-2.5" />
82
+ Done
83
+ </span>
84
+ )}
85
+
86
+ {collapsed ? (
87
+ <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
88
+ ) : (
89
+ <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
90
+ )}
91
+ </div>
92
+ </div>
93
+ </button>
94
+ </Collapsible.Trigger>
95
+
96
+ {headerActions ? (
97
+ <div className="flex shrink-0 flex-wrap items-center justify-end gap-1.5 pt-1">
98
+ {headerActions}
99
+ </div>
100
+ ) : null}
101
+ </div>
102
+
103
+ {collapsed && collapsedPreview ? (
104
+ <div className="line-clamp-2 px-4 pb-4 text-sm leading-6 text-muted-foreground">
105
+ {collapsedPreview}
106
+ </div>
107
+ ) : null}
108
+
109
+ <Collapsible.Content className="overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp">
110
+ <div className="border-t border-[var(--border-subtle)] px-4 pb-4 pt-3">{children}</div>
111
+ </Collapsible.Content>
112
+ </div>
113
+ </Collapsible.Root>
114
+ );
115
+ }
package/src/run/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { RunGroup, type RunGroupProps } from "./run-group";
2
+ export { AssistantRunShell, type AssistantRunShellProps } from "./assistant-run-shell";
2
3
  export { InlineToolItem, type InlineToolItemProps } from "./inline-tool-item";
3
4
  export {
4
5
  InlineThinkingItem,
@@ -9,5 +10,8 @@ export {
9
10
  type ExpandedToolDetailProps,
10
11
  } from "./expanded-tool-detail";
11
12
  export { LiveDuration } from "./run-item-primitives";
12
- export { ToolCallStep, ToolCallGroup, type ToolCallStepProps, type ToolCallGroupProps, type ToolCallType, type ToolCallStatus } from "./tool-call-step";
13
+ // ToolCallStep/ToolCallGroup are internal adapters over InlineToolItem (used by
14
+ // AgentTimeline + ToolCallFeed); only their status/type vocabulary is public
15
+ // because ToolCallData references it.
16
+ export { type ToolCallType, type ToolCallStatus } from "./tool-call-step";
13
17
  export { ToolCallFeed, parseToolEvent, type ToolCallFeedProps, type ToolCallData, type FeedSegment } from "./tool-call-feed";
@@ -1,10 +1,7 @@
1
1
  import { memo, useMemo, type ComponentType, type ReactNode } from "react";
2
- import * as Collapsible from "@radix-ui/react-collapsible";
3
2
  import {
4
3
  Bot,
5
4
  Loader2,
6
- ChevronDown,
7
- ChevronRight,
8
5
  Terminal,
9
6
  FileEdit,
10
7
  FileSearch,
@@ -13,10 +10,8 @@ import {
13
10
  Globe,
14
11
  ClipboardList,
15
12
  Settings,
16
- Sparkles,
17
13
  type LucideProps,
18
14
  } from "lucide-react";
19
- import { cn } from "../lib/utils";
20
15
  import { formatDuration } from "../utils/format";
21
16
  import type { Run, ToolCategory } from "../types/run";
22
17
  import type { SessionPart, ToolPart, ReasoningPart } from "../types/parts";
@@ -24,6 +19,7 @@ import type { AgentBranding } from "../types/branding";
24
19
  import type { CustomToolRenderer } from "../types/tool-display";
25
20
  import { InlineToolItem } from "./inline-tool-item";
26
21
  import { InlineThinkingItem } from "./inline-thinking-item";
22
+ import { AssistantRunShell } from "./assistant-run-shell";
27
23
  import { Markdown } from "../markdown/markdown";
28
24
  import {
29
25
  OpenUIArtifactRenderer,
@@ -393,73 +389,16 @@ export const RunGroup = memo(
393
389
  }
394
390
 
395
391
  return (
396
- <Collapsible.Root open={!collapsed} onOpenChange={() => onToggle()}>
397
- <div className="rounded-[28px] border border-[var(--border-subtle)] bg-[var(--bg-card)] shadow-none">
398
- {/* Header */}
399
- <div className="flex items-start gap-3 px-3 py-2.5">
400
- <Collapsible.Trigger asChild>
401
- <button
402
- className={cn(
403
- "w-full rounded-[20px] px-0 py-0 text-left transition-colors",
404
- "bg-transparent hover:bg-transparent",
405
- )}
406
- >
407
- <div className="flex items-center gap-2">
408
- <span className={cn("font-semibold text-sm", branding.textClass)}>
409
- {branding.label}
410
- </span>
411
-
412
- {renderSummary(run) ? (
413
- <span className="text-[11px] text-muted-foreground">{renderSummary(run)}</span>
414
- ) : null}
415
- {collapsed && run.summaryText ? (
416
- <span className="min-w-0 truncate text-[11px] text-foreground/70">
417
- {run.summaryText}
418
- </span>
419
- ) : null}
420
-
421
- <div className="ml-auto flex shrink-0 items-center gap-1.5">
422
- <CategoryBadges categories={stats.toolCategories} />
423
-
424
- {isStreaming ? (
425
- <span className="inline-flex items-center gap-1 rounded-full border border-[var(--border-accent)] bg-[var(--accent-surface-soft)] px-2 py-px text-[10px] font-semibold uppercase text-[var(--accent-text)]">
426
- <Loader2 className="h-2.5 w-2.5 animate-spin" />
427
- Running
428
- </span>
429
- ) : (
430
- <span className="inline-flex items-center gap-1 rounded-full border border-border px-2 py-px text-[10px] font-semibold uppercase text-muted-foreground">
431
- <Sparkles className="h-2.5 w-2.5" />
432
- Done
433
- </span>
434
- )}
435
-
436
- {!collapsed ? (
437
- <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
438
- ) : (
439
- <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
440
- )}
441
- </div>
442
- </div>
443
- </button>
444
- </Collapsible.Trigger>
445
-
446
- {headerActions ? (
447
- <div className="flex shrink-0 flex-wrap items-center justify-end gap-1.5 pt-1">
448
- {headerActions}
449
- </div>
450
- ) : null}
451
- </div>
452
-
453
- {/* Summary text when collapsed */}
454
- {collapsed && run.summaryText && (
455
- <div className="px-4 pb-4 text-sm leading-6 text-muted-foreground line-clamp-2">
456
- {run.summaryText}
457
- </div>
458
- )}
459
-
460
- {/* Expanded content */}
461
- <Collapsible.Content className="overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp">
462
- <div className={cn("border-t border-[var(--border-subtle)] px-4 pb-4 pt-3")}>
392
+ <AssistantRunShell
393
+ label={branding.label}
394
+ summary={renderSummary(run) || undefined}
395
+ collapsedPreview={run.summaryText ?? undefined}
396
+ badges={<CategoryBadges categories={stats.toolCategories} />}
397
+ isStreaming={isStreaming}
398
+ collapsed={collapsed}
399
+ onToggle={onToggle}
400
+ headerActions={headerActions}
401
+ >
463
402
  {allParts.map(({ part, msgId, index }, partIndex) => {
464
403
  const key = `${msgId}-${index}`;
465
404
 
@@ -549,10 +488,7 @@ export const RunGroup = memo(
549
488
  </div>
550
489
  );
551
490
  })}
552
- </div>
553
- </Collapsible.Content>
554
- </div>
555
- </Collapsible.Root>
491
+ </AssistantRunShell>
556
492
  );
557
493
  },
558
494
  );
@@ -1,11 +1,9 @@
1
1
  /**
2
- * ToolCallStep — a single agent tool invocation as a collapsible activity step.
3
- *
4
- * Now a thin adapter over the canonical `InlineToolItem`: it maps the flat
5
- * `ToolCallData`-style props (label / status / detail / output / duration) onto
6
- * a `ToolPart` and delegates rendering, so the timeline feed (`AgentTimeline`,
7
- * `ToolCallFeed`) and the run group share ONE row implementation and one look.
8
- * The bespoke row markup is gone; only the prop adapter remains.
2
+ * ToolCallStep — internal adapter over the canonical `InlineToolItem` row.
3
+ * Maps flat `ToolCallData`-style props (label / status / detail / output /
4
+ * duration) onto a `ToolPart` so `AgentTimeline` and `ToolCallFeed` share the
5
+ * one row implementation. Not exported publicly; only the `ToolCallType` /
6
+ * `ToolCallStatus` vocabulary is (via `ToolCallData`).
9
7
  */
10
8
 
11
9
  import { type ReactNode } from "react";
@@ -1,142 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react'
2
- import { ChatInput, type PendingFile } from './chat-input'
3
-
4
- const meta: Meta<typeof ChatInput> = {
5
- title: 'Chat/ChatInput',
6
- component: ChatInput,
7
- parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
8
- decorators: [
9
- (Story) => (
10
- <div className="w-[680px]">
11
- <Story />
12
- </div>
13
- ),
14
- ],
15
- args: {
16
- onSend: (msg, files) => console.log('send', msg, files),
17
- onCancel: () => console.log('cancel'),
18
- },
19
- }
20
-
21
- export default meta
22
- type Story = StoryObj<typeof ChatInput>
23
-
24
- export const Empty: Story = {
25
- name: 'Empty — ready',
26
- }
27
-
28
- export const WithModelSelector: Story = {
29
- name: 'With model selector',
30
- args: {
31
- modelLabel: 'claude-sonnet-4-6',
32
- onModelClick: () => console.log('model click'),
33
- },
34
- }
35
-
36
- export const WithAttachButtons: Story = {
37
- name: 'With attach buttons',
38
- args: {
39
- onAttach: (files) => console.log('attach', files),
40
- onAttachFolder: (files) => console.log('attach folder', files),
41
- modelLabel: 'claude-sonnet-4-6',
42
- onModelClick: () => console.log('model click'),
43
- },
44
- }
45
-
46
- export const Streaming: Story = {
47
- name: 'Streaming — stop button active',
48
- args: {
49
- isStreaming: true,
50
- onCancel: () => console.log('cancel'),
51
- onAttach: (files) => console.log('attach', files),
52
- modelLabel: 'claude-sonnet-4-6',
53
- },
54
- }
55
-
56
- export const Disabled: Story = {
57
- name: 'Disabled',
58
- args: {
59
- disabled: true,
60
- onAttach: (files) => console.log('attach', files),
61
- modelLabel: 'claude-sonnet-4-6',
62
- },
63
- }
64
-
65
- const sampleFiles: PendingFile[] = [
66
- { id: 'f1', name: 'dataset.csv', size: 204800, type: 'file', status: 'ready' },
67
- { id: 'f2', name: 'report.pdf', size: 1572864, type: 'file', status: 'uploading' },
68
- ]
69
-
70
- export const WithFilesAttached: Story = {
71
- name: 'With file attachments',
72
- args: {
73
- onAttach: (files) => console.log('attach', files),
74
- pendingFiles: sampleFiles,
75
- onRemoveFile: (id) => console.log('remove', id),
76
- modelLabel: 'claude-sonnet-4-6',
77
- onModelClick: () => console.log('model click'),
78
- },
79
- }
80
-
81
- const folderFiles: PendingFile[] = [
82
- { id: 'd1', name: 'my-project', size: 0, type: 'folder', fileCount: 42, status: 'ready' },
83
- { id: 'f3', name: 'notes.txt', size: 1024, type: 'file', status: 'ready' },
84
- ]
85
-
86
- export const WithFolderAttached: Story = {
87
- name: 'With folder + file',
88
- args: {
89
- onAttach: (files) => console.log('attach', files),
90
- onAttachFolder: (files) => console.log('attach folder', files),
91
- pendingFiles: folderFiles,
92
- onRemoveFile: (id) => console.log('remove', id),
93
- modelLabel: 'claude-sonnet-4-6',
94
- },
95
- }
96
-
97
- export const WithErrorFile: Story = {
98
- name: 'With error attachment',
99
- args: {
100
- onAttach: (files) => console.log('attach', files),
101
- pendingFiles: [
102
- { id: 'e1', name: 'too-large.zip', size: 524288000, type: 'file', status: 'error' },
103
- { id: 'f4', name: 'config.json', size: 2048, type: 'file', status: 'ready' },
104
- ],
105
- onRemoveFile: (id) => console.log('remove', id),
106
- },
107
- }
108
-
109
- export const CustomPlaceholder: Story = {
110
- name: 'Custom placeholder',
111
- args: {
112
- placeholder: 'Describe the data transformation you need…',
113
- onAttach: (files) => console.log('attach', files),
114
- modelLabel: 'gpt-4o',
115
- },
116
- }
117
-
118
- /** Static drag-over overlay preview */
119
- export const DragOverPreview: Story = {
120
- name: 'Drag-over overlay (static preview)',
121
- render: (args) => (
122
- <div className="w-[680px] px-4 py-3 relative">
123
- {/* Drag overlay replica */}
124
- <div className="absolute inset-0 z-10 flex items-center justify-center rounded-[28px] border-2 border-dashed border-blue-400 bg-blue-500/8 backdrop-blur-sm pointer-events-none mx-4 my-3">
125
- <div className="text-center">
126
- <div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-xl bg-blue-500/15">
127
- <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-blue-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
128
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
129
- <polyline points="17 8 12 3 7 8" />
130
- <line x1="12" y1="3" x2="12" y2="15" />
131
- </svg>
132
- </div>
133
- <p className="text-sm font-semibold text-white">Drop files to add context</p>
134
- <p className="mt-1 text-xs text-zinc-400">Files will be attached to your next message.</p>
135
- </div>
136
- </div>
137
- {/* Underlying input (blurred) */}
138
- <ChatInput {...args} onAttach={() => {}} />
139
- </div>
140
- ),
141
- args: {},
142
- }