@tangle-network/ui 11.51.15 → 11.51.17

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 (78) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/chat/thinking-indicator.js +9 -15
  3. package/dist/editor/tiptap-editor.js +4 -4
  4. package/dist/files/file-preview.js +1 -1
  5. package/dist/hooks/use-phone.d.ts +4 -0
  6. package/dist/hooks/use-phone.js +14 -0
  7. package/dist/hooks.d.ts +2 -1
  8. package/dist/hooks.js +3 -2
  9. package/dist/index.d.ts +4 -2
  10. package/dist/index.js +5 -3
  11. package/dist/openui/openui-artifact-renderer.js +7 -7
  12. package/dist/presentation.d.ts +1 -1
  13. package/dist/presentation.js +1 -1
  14. package/dist/primitives/artifact-pane.js +1 -1
  15. package/dist/primitives/heading.js +1 -1
  16. package/dist/primitives/progress.js +1 -1
  17. package/dist/primitives/sidebar-drop-zone.js +1 -1
  18. package/dist/primitives/stat-card.js +1 -1
  19. package/dist/primitives/toast.js +1 -1
  20. package/dist/primitives/upload-progress.js +2 -2
  21. package/dist/primitives/wizard.js +2 -2
  22. package/dist/run/expandable-content.js +1 -1
  23. package/dist/run/expanded-tool-detail.js +3 -3
  24. package/dist/run/run-group.js +11 -14
  25. package/dist/run/run-item-primitives.d.ts +3 -0
  26. package/dist/run/run-item-primitives.js +12 -0
  27. package/dist/run/run-row-shell.d.ts +9 -0
  28. package/dist/run/run-row-shell.js +14 -1
  29. package/dist/run.d.ts +3 -2
  30. package/dist/run.js +3 -2
  31. package/dist/tool-previews/command-preview.js +3 -3
  32. package/dist/tool-previews/diff-preview.js +2 -2
  33. package/dist/tool-previews/question-preview.js +1 -1
  34. package/package.json +1 -1
  35. package/src/chat/thinking-indicator.tsx +2 -4
  36. package/src/editor/document-editor-pane.stories.tsx +1 -1
  37. package/src/editor/tiptap-editor.tsx +4 -4
  38. package/src/files/file-preview.tsx +1 -1
  39. package/src/hooks/index.ts +1 -0
  40. package/src/hooks/use-phone.ts +18 -0
  41. package/src/markdown/markdown.stories.tsx +1 -1
  42. package/src/openui/openui-artifact-renderer.tsx +7 -7
  43. package/src/presentation.ts +1 -1
  44. package/src/primitives/artifact-pane.tsx +1 -1
  45. package/src/primitives/avatar.stories.tsx +3 -3
  46. package/src/primitives/badge.stories.tsx +3 -3
  47. package/src/primitives/card.stories.tsx +1 -1
  48. package/src/primitives/compliance-badge.stories.tsx +3 -3
  49. package/src/primitives/dropdown-menu.stories.tsx +1 -1
  50. package/src/primitives/heading.tsx +1 -1
  51. package/src/primitives/input.stories.tsx +2 -2
  52. package/src/primitives/label.stories.tsx +3 -3
  53. package/src/primitives/progress.stories.tsx +5 -5
  54. package/src/primitives/progress.tsx +1 -1
  55. package/src/primitives/select.stories.tsx +3 -3
  56. package/src/primitives/sidebar-drop-zone.stories.tsx +1 -1
  57. package/src/primitives/sidebar-drop-zone.tsx +1 -1
  58. package/src/primitives/skeleton.stories.tsx +4 -4
  59. package/src/primitives/stat-card.stories.tsx +22 -33
  60. package/src/primitives/stat-card.tsx +1 -1
  61. package/src/primitives/status-pill.test.tsx +0 -52
  62. package/src/primitives/switch.stories.tsx +2 -2
  63. package/src/primitives/tabs.stories.tsx +3 -3
  64. package/src/primitives/toast.tsx +1 -1
  65. package/src/primitives/upload-progress.tsx +2 -2
  66. package/src/primitives/wizard.tsx +1 -1
  67. package/src/run/expandable-content.tsx +1 -1
  68. package/src/run/expanded-tool-detail.tsx +3 -3
  69. package/src/run/index.ts +2 -0
  70. package/src/run/inline-thinking-item.stories.tsx +3 -3
  71. package/src/run/inline-tool-item.stories.tsx +3 -3
  72. package/src/run/run-group.tsx +9 -9
  73. package/src/run/run-item-primitives.tsx +12 -0
  74. package/src/run/run-row-shell.tsx +16 -0
  75. package/src/styles/presentation.css +1 -1
  76. package/src/tool-previews/command-preview.tsx +3 -3
  77. package/src/tool-previews/diff-preview.tsx +2 -2
  78. package/src/tool-previews/question-preview.tsx +1 -1
@@ -1,5 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react'
2
- import { Activity, Cpu, DollarSign, HardDrive, Terminal, Users, Zap } from 'lucide-react'
2
+ import { Terminal } from 'lucide-react'
3
3
  import { StatCard } from './stat-card'
4
4
 
5
5
  const meta: Meta<typeof StatCard> = {
@@ -13,7 +13,7 @@ type Story = StoryObj<typeof StatCard>
13
13
 
14
14
  export const Default: Story = {
15
15
  args: {
16
- title: 'Active Sessions',
16
+ title: 'Active sessions',
17
17
  value: 24,
18
18
  subtitle: 'across 3 regions',
19
19
  icon: <Terminal className="h-5 w-5" />,
@@ -21,10 +21,10 @@ export const Default: Story = {
21
21
  }
22
22
 
23
23
  export const SandboxVariant: Story = {
24
- name: 'Sandbox Variant',
24
+ name: 'Sandbox variant',
25
25
  args: {
26
26
  variant: 'sandbox',
27
- title: 'Active Sessions',
27
+ title: 'Active sessions',
28
28
  value: 24,
29
29
  subtitle: 'across 3 regions',
30
30
  icon: <Terminal className="h-5 w-5" />,
@@ -33,103 +33,92 @@ export const SandboxVariant: Story = {
33
33
  }
34
34
 
35
35
  export const CpuUsage: Story = {
36
- name: 'CPU Usage',
36
+ name: 'CPU usage',
37
37
  args: {
38
- title: 'Avg CPU Usage',
38
+ title: 'Avg CPU usage',
39
39
  value: '68%',
40
40
  subtitle: 'p95: 94%',
41
- icon: <Cpu className="h-5 w-5" />,
42
41
  trend: { value: -4, label: 'vs yesterday' },
43
42
  },
44
43
  }
45
44
 
46
45
  export const MemoryUsage: Story = {
47
- name: 'Memory Usage',
46
+ name: 'Memory usage',
48
47
  args: {
49
- title: 'Memory Allocated',
48
+ title: 'Memory allocated',
50
49
  value: '12.4 GB',
51
50
  subtitle: 'of 32 GB pool',
52
- icon: <HardDrive className="h-5 w-5" />,
53
51
  trend: { value: 8, label: 'vs last hour' },
54
52
  },
55
53
  }
56
54
 
57
55
  export const MonthlyCost: Story = {
58
- name: 'Monthly Cost',
56
+ name: 'Monthly cost',
59
57
  args: {
60
- title: 'Estimated Cost',
58
+ title: 'Estimated cost',
61
59
  value: '$142.80',
62
60
  subtitle: 'current billing cycle',
63
- icon: <DollarSign className="h-5 w-5" />,
64
61
  trend: { value: 0, label: 'vs last month' },
65
62
  },
66
63
  }
67
64
 
68
65
  export const TrendPositive: Story = {
69
- name: 'Trend — Positive',
66
+ name: 'Trend up',
70
67
  args: {
71
- title: 'Sessions Today',
68
+ title: 'Sessions today',
72
69
  value: 1284,
73
- icon: <Activity className="h-5 w-5" />,
74
70
  trend: { value: 23, label: 'vs yesterday' },
75
71
  },
76
72
  }
77
73
 
78
74
  export const TrendNegative: Story = {
79
- name: 'Trend — Negative',
75
+ name: 'Trend down',
80
76
  args: {
81
- title: 'Error Rate',
77
+ title: 'Error rate',
82
78
  value: '1.2%',
83
- icon: <Zap className="h-5 w-5" />,
84
79
  trend: { value: -18, label: 'vs last week' },
85
80
  },
86
81
  }
87
82
 
88
83
  export const InfraGrid: Story = {
89
- name: 'Infrastructure Grid',
84
+ name: 'Infrastructure grid',
90
85
  render: () => (
91
86
  <div className="grid grid-cols-2 gap-4 w-[640px]">
92
87
  <StatCard
93
- title="Active Sessions"
88
+ title="Active sessions"
94
89
  value={24}
95
90
  subtitle="across 3 regions"
96
- icon={<Terminal className="h-5 w-5" />}
97
91
  trend={{ value: 12, label: 'vs last hour' }}
98
92
  />
99
93
  <StatCard
100
- title="CPU Usage"
94
+ title="CPU usage"
101
95
  value="68%"
102
96
  subtitle="p95: 94%"
103
- icon={<Cpu className="h-5 w-5" />}
104
97
  trend={{ value: -4, label: 'vs yesterday' }}
105
98
  />
106
99
  <StatCard
107
- title="Memory Allocated"
100
+ title="Memory allocated"
108
101
  value="12.4 GB"
109
102
  subtitle="of 32 GB pool"
110
- icon={<HardDrive className="h-5 w-5" />}
111
103
  trend={{ value: 8, label: 'vs last hour' }}
112
104
  />
113
105
  <StatCard
114
106
  variant="sandbox"
115
- title="Estimated Cost"
107
+ title="Estimated cost"
116
108
  value="$142.80"
117
109
  subtitle="current billing cycle"
118
- icon={<DollarSign className="h-5 w-5" />}
119
110
  trend={{ value: 3, label: 'vs last month' }}
120
111
  />
121
112
  <StatCard
122
- title="Unique Users"
113
+ title="Unique users"
123
114
  value={312}
124
115
  subtitle="last 30 days"
125
- icon={<Users className="h-5 w-5" />}
126
- trend={{ value: 19, label: 'MoM' }}
116
+ trend={{ value: 19, label: 'vs last month' }}
127
117
  />
128
118
  <StatCard
129
- title="Avg Startup Time"
119
+ title="Avg startup time"
130
120
  value="840ms"
131
121
  subtitle="p99: 1.4s"
132
- icon={<Zap className="h-5 w-5" />}
133
122
  trend={{ value: -11, label: 'vs last week' }}
134
123
  />
135
124
  </div>
@@ -57,7 +57,7 @@ const StatCard = React.forwardRef<HTMLDivElement, StatCardProps>(
57
57
  <div className="flex items-start justify-between">
58
58
  <div className="space-y-1">
59
59
  <p className="text-muted-foreground text-sm">{title}</p>
60
- <p className="font-bold text-3xl tracking-tight">{value}</p>
60
+ <p className="font-medium text-3xl tabular-nums tracking-tight">{value}</p>
61
61
  {/* A token, never `opacity-70` over the muted tone: a translucent
62
62
  foreground renders as the token COMPOSITED over whatever plane sits
63
63
  behind it, so the same class lands a different colour — and a
@@ -28,31 +28,6 @@ describe("StatusPill", () => {
28
28
  expect(glyph).toHaveAttribute("aria-hidden", "true");
29
29
  });
30
30
 
31
- it.each(TONES)("brings %s's own paired background", (tone) => {
32
- const { container } = render(<StatusPill tone={tone}>Status</StatusPill>);
33
- const pill = container.firstElementChild as HTMLElement;
34
- expect(pill.className).toContain(`bg-[var(--surface-`);
35
- expect(pill.className).toContain("border");
36
- });
37
-
38
- // A tone colour is solved against its own background. `bare` drops that
39
- // background, so the tone may ride on the glyph only — lifting it onto the
40
- // label would put it on whatever plane the caller happens to be on.
41
- it.each(TONES)("keeps %s's tone off the label in bare mode", (tone) => {
42
- const { container } = render(
43
- <StatusPill bare tone={tone}>
44
- Status
45
- </StatusPill>,
46
- );
47
- const pill = container.firstElementChild as HTMLElement;
48
- expect(pill.className).toContain("bg-transparent");
49
- expect(pill.className).not.toContain("bg-[var(--surface-");
50
-
51
- const glyphWrapper = container.querySelector("svg")
52
- ?.parentElement as HTMLElement;
53
- expect(glyphWrapper.className).toContain("text-[var(--surface-");
54
- });
55
-
56
31
  // Guards the reason TONE_TEXT is declared rather than recovered by searching
57
32
  // TONE_SURFACE for a `text-` class: a miss there yields undefined and removes
58
33
  // the tone from the only channel bare mode has left.
@@ -68,22 +43,6 @@ describe("StatusPill", () => {
68
43
  expect(glyphWrapper.className).toMatch(/text-\[var\(--surface-[a-z]+-text\)\]/);
69
44
  });
70
45
 
71
- // `px-2` from the size and `px-0` from bare are the same utility group, so
72
- // which one lands is decided by the class merge rather than by reading order.
73
- // Asserting the resolved class list keeps that a guarantee: a bare pill has to
74
- // sit flush against the text beside it, and silently keeping the size padding
75
- // would space it like a chip that lost only its background.
76
- it.each(["sm", "md"] as const)("drops the %s size padding in bare mode", (size) => {
77
- const { container } = render(
78
- <StatusPill bare size={size} tone="info">
79
- Queued
80
- </StatusPill>,
81
- );
82
- const cls = (container.firstElementChild as HTMLElement).className;
83
- expect(cls).toContain("px-0");
84
- expect(cls).not.toMatch(/(^| )px-2(\.5)?( |$)/);
85
- });
86
-
87
46
  it("gives no two tones the same silhouette", () => {
88
47
  const shapes = TONES.map((tone) => {
89
48
  const { container } = render(<StatusPill tone={tone}>S</StatusPill>);
@@ -91,15 +50,4 @@ describe("StatusPill", () => {
91
50
  });
92
51
  expect(new Set(shapes).size).toBe(shapes.length);
93
52
  });
94
-
95
- it("takes a larger size", () => {
96
- const { container } = render(
97
- <StatusPill size="md" tone="info">
98
- Queued
99
- </StatusPill>,
100
- );
101
- expect((container.firstElementChild as HTMLElement).className).toContain(
102
- "text-sm",
103
- );
104
- });
105
53
  });
@@ -41,7 +41,7 @@ export const Overview: Story = {
41
41
  name: 'Overview',
42
42
  render: () => (
43
43
  <div className="flex flex-col gap-6 p-6 w-80">
44
- <div className="text-muted-foreground text-xs font-mono uppercase tracking-widest">States</div>
44
+ <div className="text-muted-foreground text-xs font-mono">States</div>
45
45
  <div className="flex flex-col gap-4">
46
46
  <div className="flex items-center justify-between">
47
47
  <span className="text-sm text-muted-foreground">Off</span>
@@ -61,7 +61,7 @@ export const Overview: Story = {
61
61
  </div>
62
62
  </div>
63
63
 
64
- <div className="text-muted-foreground text-xs font-mono uppercase tracking-widest mt-2">Settings panel</div>
64
+ <div className="text-muted-foreground text-xs font-mono mt-2">Settings panel</div>
65
65
  <div className="rounded-xl border border-border bg-card p-4 flex flex-col gap-4">
66
66
  {[
67
67
  { id: 'auto-sleep', label: 'Auto-sleep after idle', checked: true },
@@ -104,7 +104,7 @@ export const Overview: Story = {
104
104
  render: () => (
105
105
  <div className="flex flex-col gap-10 p-6 w-[480px]">
106
106
  <div>
107
- <div className="text-muted-foreground text-xs font-mono uppercase tracking-widest mb-4">Default (segmented)</div>
107
+ <div className="text-muted-foreground text-xs font-mono mb-4">Default (segmented)</div>
108
108
  <Tabs defaultValue="overview">
109
109
  <TabsList>
110
110
  <TabsTrigger value="overview">Overview</TabsTrigger>
@@ -137,7 +137,7 @@ export const Overview: Story = {
137
137
  </div>
138
138
 
139
139
  <div>
140
- <div className="text-muted-foreground text-xs font-mono uppercase tracking-widest mb-4">Pills</div>
140
+ <div className="text-muted-foreground text-xs font-mono mb-4">Pills</div>
141
141
  <Tabs defaultValue="all">
142
142
  <TabsList variant="pills">
143
143
  <TabsTrigger value="all" variant="pills">All</TabsTrigger>
@@ -157,7 +157,7 @@ export const Overview: Story = {
157
157
  </div>
158
158
 
159
159
  <div>
160
- <div className="text-muted-foreground text-xs font-mono uppercase tracking-widest mb-4">Underline</div>
160
+ <div className="text-muted-foreground text-xs font-mono mb-4">Underline</div>
161
161
  <Tabs defaultValue="settings">
162
162
  <TabsList variant="underline">
163
163
  <TabsTrigger value="settings" variant="underline">Settings</TabsTrigger>
@@ -13,7 +13,7 @@ import { focusRing } from "../lib/focus";
13
13
  import { cn } from "../lib/utils";
14
14
 
15
15
  const toastVariants = cva(
16
- "pointer-events-auto relative flex w-full items-center justify-between gap-3 overflow-hidden rounded-lg border p-4 shadow-lg transition-all",
16
+ "pointer-events-auto relative flex w-full items-center justify-between gap-3 overflow-hidden rounded-lg border p-4 shadow-lg transition-[transform,opacity]",
17
17
  {
18
18
  variants: {
19
19
  variant: {
@@ -67,8 +67,8 @@ export function UploadProgress({ files, onRemove, onRetry, className }: UploadPr
67
67
  {file.status === "uploading" && file.progress !== undefined && (
68
68
  <div className="mt-1 h-1 w-full overflow-hidden rounded-full bg-muted/50">
69
69
  <div
70
- className="h-full rounded-full bg-primary transition-all"
71
- style={{ width: `${file.progress}%` }}
70
+ className="h-full w-full rounded-full bg-primary transition-transform duration-300 ease-out"
71
+ style={{ transform: `translateX(-${100 - file.progress}%)` }}
72
72
  />
73
73
  </div>
74
74
  )}
@@ -137,7 +137,7 @@ export function Wizard({ children, step, defaultStep = 0, onStepChange, onFinish
137
137
  </p>
138
138
  <div className="mt-2 h-1 overflow-hidden rounded-full bg-muted" role="progressbar"
139
139
  aria-valuemin={1} aria-valuemax={total} aria-valuenow={current + 1} aria-label={`Step ${current + 1} of ${total}`}>
140
- <div className="h-full rounded-full bg-primary transition-all" style={{ width: `${((current + 1) / total) * 100}%` }} />
140
+ <div className="h-full w-full rounded-full bg-primary transition-transform duration-300 ease-out" style={{ transform: `translateX(-${100 - ((current + 1) / total) * 100}%)` }} />
141
141
  </div>
142
142
  </div>
143
143
  {steps.map((child, index) => index === current
@@ -68,7 +68,7 @@ export function ExpandableContent({
68
68
  type="button"
69
69
  onClick={() => setExpanded((value) => !value)}
70
70
  className={cn(
71
- "mt-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground",
71
+ "mt-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
72
72
  toggleClassName,
73
73
  )}
74
74
  >
@@ -128,7 +128,7 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
128
128
  <div className="overflow-hidden rounded-[var(--radius-lg)] border border-border bg-card">
129
129
  <div className="flex items-center gap-2 border-b border-border bg-background px-3 py-2">
130
130
  <ArrowRight className="h-3 w-3 text-primary" />
131
- <span className="text-xs font-medium uppercase tracking-[0.08em] text-muted-foreground">
131
+ <span className="text-xs font-medium text-muted-foreground">
132
132
  Input
133
133
  </span>
134
134
  </div>
@@ -141,7 +141,7 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
141
141
  <div className="overflow-hidden rounded-[var(--radius-lg)] border border-border bg-card">
142
142
  <div className="flex items-center gap-2 border-b border-border bg-background px-3 py-2">
143
143
  <ArrowLeft className="h-3 w-3 text-primary" />
144
- <span className="text-xs font-medium uppercase tracking-[0.08em] text-muted-foreground">
144
+ <span className="text-xs font-medium text-muted-foreground">
145
145
  Output
146
146
  </span>
147
147
  </div>
@@ -164,7 +164,7 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
164
164
  <div className="overflow-hidden rounded-[var(--radius-lg)] border border-[var(--surface-danger-border)]">
165
165
  <div className="flex items-center gap-2 border-b border-[var(--surface-danger-border)] bg-[var(--surface-danger-bg)] px-3 py-2">
166
166
  <RunStatusIcon status="failed" aria-hidden className="size-3" />
167
- <span className="text-xs font-medium uppercase tracking-[0.08em] text-[var(--surface-danger-text)]">
167
+ <span className="text-xs font-medium text-[var(--surface-danger-text)]">
168
168
  Error
169
169
  </span>
170
170
  </div>
package/src/run/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { RunGroup, type RunGroupProps } from "./run-group";
2
2
  export {
3
3
  RunRowShell,
4
+ RunRowStatusDot,
4
5
  type RunRowShellProps,
5
6
  type RunRowStatus,
6
7
  } from "./run-row-shell";
@@ -13,6 +14,7 @@ export {
13
14
  ExpandedToolDetail,
14
15
  type ExpandedToolDetailProps,
15
16
  } from "./expanded-tool-detail";
17
+ export { LiveDuration } from "./run-item-primitives";
16
18
  // ToolCallStep/ToolCallGroup are internal adapters over InlineToolItem (used by
17
19
  // AgentTimeline + ToolCallFeed); only their status/type vocabulary is public
18
20
  // because ToolCallData references it.
@@ -154,11 +154,11 @@ export const UnifiedWithToolRow: Story = {
154
154
  export const AllStates: Story = {
155
155
  render: () => (
156
156
  <div className="p-6 max-w-2xl space-y-3">
157
- <p className="text-xs font-mono uppercase tracking-widest text-muted-foreground px-1">Active</p>
157
+ <p className="text-xs font-mono text-muted-foreground px-1">Active</p>
158
158
  <InlineThinkingItem part={thinkingActive} defaultOpen={true} autoCollapse={false} />
159
- <p className="text-xs font-mono uppercase tracking-widest text-muted-foreground px-1 pt-2">Complete (collapsed)</p>
159
+ <p className="text-xs font-mono text-muted-foreground px-1 pt-2">Complete (collapsed)</p>
160
160
  <InlineThinkingItem part={thinkingComplete} defaultOpen={false} autoCollapse={false} />
161
- <p className="text-xs font-mono uppercase tracking-widest text-muted-foreground px-1 pt-2">Complete (expanded)</p>
161
+ <p className="text-xs font-mono text-muted-foreground px-1 pt-2">Complete (expanded)</p>
162
162
  <InlineThinkingItem part={thinkingLong} defaultOpen={true} autoCollapse={false} />
163
163
  </div>
164
164
  ),
@@ -211,11 +211,11 @@ export const GroupedSequence: Story = {
211
211
  export const AllStates: Story = {
212
212
  render: () => (
213
213
  <div className="p-6 max-w-2xl space-y-3">
214
- <p className="text-xs font-mono uppercase tracking-widest text-muted-foreground px-1">Completed</p>
214
+ <p className="text-xs font-mono text-muted-foreground px-1">Completed</p>
215
215
  <InlineToolItem part={bashComplete} groupPosition="single" />
216
- <p className="text-xs font-mono uppercase tracking-widest text-muted-foreground px-1 pt-2">Running</p>
216
+ <p className="text-xs font-mono text-muted-foreground px-1 pt-2">Running</p>
217
217
  <InlineToolItem part={bashRunning} groupPosition="single" />
218
- <p className="text-xs font-mono uppercase tracking-widest text-muted-foreground px-1 pt-2">Error</p>
218
+ <p className="text-xs font-mono text-muted-foreground px-1 pt-2">Error</p>
219
219
  <InlineToolItem part={bashError} groupPosition="single" />
220
220
  </div>
221
221
  ),
@@ -80,7 +80,7 @@ const DEFAULT_BRANDING: AgentBranding = {
80
80
  };
81
81
 
82
82
  const ASSISTANT_SHELL =
83
- "min-w-0 flex-1 space-y-3 rounded-[26px] border border-[var(--border-subtle)] bg-[color:color-mix(in_srgb,var(--bg-card)_94%,transparent)] px-5 py-4 shadow-[0_1px_2px_rgba(15,23,42,0.04)]";
83
+ "min-w-0 flex-1 space-y-3 rounded-[var(--radius-panel)] border border-[var(--border-subtle)] bg-[color:color-mix(in_srgb,var(--bg-card)_94%,transparent)] px-5 py-4 shadow-[0_1px_2px_rgba(15,23,42,0.04)]";
84
84
 
85
85
  function AssistantShell({
86
86
  branding,
@@ -251,7 +251,7 @@ function CategoryBadges({ categories }: { categories: Set<ToolCategory> }) {
251
251
  <span
252
252
  key={cat}
253
253
  title={cat}
254
- className="flex h-5 w-5 items-center justify-center rounded border border-border text-muted-foreground"
254
+ className="flex h-5 w-5 items-center justify-center text-muted-foreground"
255
255
  >
256
256
  <Icon className="h-3 w-3" />
257
257
  </span>
@@ -326,10 +326,10 @@ export const RunGroup = memo(
326
326
  const pendingResponse = (
327
327
  <AssistantShell branding={branding} isStreaming={true}>
328
328
  <div className="flex items-center gap-2 px-0.5 py-0.5 text-sm text-[var(--text-muted)]">
329
- <span className="flex gap-[5px]">
330
- <span className="h-2 w-2 animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "0ms" }} />
331
- <span className="h-2 w-2 animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "150ms" }} />
332
- <span className="h-2 w-2 animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "300ms" }} />
329
+ <span className="flex gap-1">
330
+ <span className="h-2 w-2 animate-[tangle-breathe_var(--motion-ambient)_infinite] motion-reduce:animate-none rounded-full bg-[var(--brand-glow)]" />
331
+ <span className="h-2 w-2 animate-[tangle-breathe_var(--motion-ambient)_infinite] motion-reduce:animate-none rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "200ms" }} />
332
+ <span className="h-2 w-2 animate-[tangle-breathe_var(--motion-ambient)_infinite] motion-reduce:animate-none rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "400ms" }} />
333
333
  </span>
334
334
  </div>
335
335
  </AssistantShell>
@@ -474,16 +474,16 @@ export const RunGroup = memo(
474
474
  {branding.label}
475
475
  </span>
476
476
  {renderSummary(run) ? (
477
- <span className="text-[11px] text-muted-foreground">{renderSummary(run)}</span>
477
+ <span className="text-xs text-muted-foreground">{renderSummary(run)}</span>
478
478
  ) : null}
479
479
  {collapsed && run.summaryText ? (
480
- <span className="min-w-0 truncate text-[11px] text-foreground/70">
480
+ <span className="min-w-0 truncate text-xs text-foreground/70">
481
481
  {run.summaryText}
482
482
  </span>
483
483
  ) : null}
484
484
  <span className="ml-auto flex shrink-0 items-center gap-1.5">
485
485
  <CategoryBadges categories={stats.toolCategories} />
486
- <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">
486
+ <span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
487
487
  <RunStatusIcon status={isStreaming ? "running" : "done"} aria-hidden className="size-3" />
488
488
  {statusMeta(isStreaming ? "running" : "done").label}
489
489
  </span>
@@ -0,0 +1,12 @@
1
+ import { useLiveTime } from "../hooks/use-live-time";
2
+ import { formatDuration } from "../utils/format";
3
+
4
+ export function LiveDuration({ startTime }: { startTime: number }) {
5
+ const elapsed = useLiveTime(100) - startTime;
6
+
7
+ return (
8
+ <span className="text-xs font-mono text-muted-foreground tabular-nums">
9
+ {formatDuration(elapsed)}
10
+ </span>
11
+ );
12
+ }
@@ -7,6 +7,22 @@ import { formatDuration } from "../utils/format";
7
7
 
8
8
  export type RunRowStatus = "running" | "success" | "error" | "idle";
9
9
 
10
+ /**
11
+ * A red dot for a failed row, for rows composed outside the shell. The shell
12
+ * itself marks a failure on the row's own glyph, beside its name, never at the
13
+ * far edge. Success is silence, and a running row carries its state in the
14
+ * title — a `TextShimmer` sweep.
15
+ */
16
+ export function RunRowStatusDot({ status }: { status: RunRowStatus }) {
17
+ if (status !== "error") return null;
18
+ return (
19
+ <span
20
+ data-run-row-status="error"
21
+ className="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--surface-danger-text)]"
22
+ />
23
+ );
24
+ }
25
+
10
26
  export interface RunRowShellProps {
11
27
  /** Semantic lead glyph (14px). At rest it is the row's only ornament; on
12
28
  * hover and while open the expand chevron takes its slot. */
@@ -5,7 +5,7 @@
5
5
  .tangle-heading[data-variant="page"] { font-size: var(--font-size-3xl); }
6
6
  .tangle-heading[data-variant="section"] { font-size: var(--font-size-xl); }
7
7
  .tangle-heading[data-variant="subsection"] { font-size: var(--font-size-base); }
8
- .tangle-heading[data-variant="eyebrow"] { font-size: var(--font-size-xs); text-transform: uppercase; letter-spacing: var(--tracking-wide); color: hsl(var(--muted-foreground)); }
8
+ .tangle-heading[data-variant="eyebrow"] { font-size: var(--font-size-xs); line-height: 1rem; font-weight: 500; letter-spacing: 0; color: hsl(var(--muted-foreground)); }
9
9
  .tangle-page-header { min-width: 0; margin-bottom: 1.5rem; }
10
10
  .tangle-page-header-main { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 1.5rem; }
11
11
  .tangle-intro .tangle-page-header-main { display: block; }
@@ -139,7 +139,7 @@ function ExitBadge({
139
139
  }) {
140
140
  if (status === "running" || status === "pending") {
141
141
  return (
142
- <span className="inline-flex shrink-0 items-center gap-1 text-[11px] text-muted-foreground">
142
+ <span className="inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground">
143
143
  <Loader2 className="h-3 w-3 animate-spin" aria-hidden="true" />
144
144
  running
145
145
  </span>
@@ -171,7 +171,7 @@ function ExitBadge({
171
171
  <span
172
172
  className={cn(
173
173
  // Every badge has a border, so mixed rows keep one height.
174
- "inline-flex shrink-0 items-center rounded-full border px-1.5 py-px font-sans text-[11px] font-medium tabular-nums",
174
+ "inline-flex shrink-0 items-center rounded-full border px-1.5 py-px font-sans text-xs font-medium tabular-nums",
175
175
  tone === "danger" &&
176
176
  "border-transparent bg-[var(--surface-danger-bg)] text-[var(--surface-danger-text)]",
177
177
  tone === "success" &&
@@ -253,7 +253,7 @@ function OutputRegion({
253
253
  type="button"
254
254
  onClick={() => setFull(true)}
255
255
  className={cn(
256
- "w-full border-t border-white/10 px-3 py-1.5 text-left font-sans text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground",
256
+ "w-full border-t border-white/10 px-3 py-1.5 text-left font-sans text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
257
257
  focusRing,
258
258
  )}
259
259
  >
@@ -64,7 +64,7 @@ export const DiffPreview = memo(({ part }: DiffPreviewProps) => {
64
64
  ) : null}
65
65
  {!payload.diff && payload.before ? (
66
66
  <div className="space-y-2">
67
- <div className="text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground">
67
+ <div className="text-xs font-medium text-muted-foreground">
68
68
  Before
69
69
  </div>
70
70
  <CodeBlock code={payload.before} language="text" className="rounded-[var(--radius-md)]" />
@@ -72,7 +72,7 @@ export const DiffPreview = memo(({ part }: DiffPreviewProps) => {
72
72
  ) : null}
73
73
  {!payload.diff && payload.after ? (
74
74
  <div className="space-y-2">
75
- <div className="text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground">
75
+ <div className="text-xs font-medium text-muted-foreground">
76
76
  After
77
77
  </div>
78
78
  <CodeBlock code={payload.after} language="text" className="rounded-[var(--radius-md)]" />
@@ -83,7 +83,7 @@ export const QuestionPreview = memo(({ part }: QuestionPreviewProps) => {
83
83
  key={`${question}-${index}`}
84
84
  className="rounded-[var(--radius-md)] border border-border bg-muted px-3 py-3"
85
85
  >
86
- <div className="text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground">
86
+ <div className="text-xs font-medium text-muted-foreground">
87
87
  Question {index + 1}
88
88
  </div>
89
89
  <div className="mt-2 text-sm leading-relaxed text-foreground">