@tangle-network/ui 5.0.0 → 5.2.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/dist/run.js CHANGED
@@ -2,20 +2,19 @@ import "./chunk-LQS34IGP.js";
2
2
  import {
3
3
  ToolCallFeed,
4
4
  parseToolEvent
5
- } from "./chunk-5VPTNXX7.js";
5
+ } from "./chunk-O6NUUCT2.js";
6
6
  import {
7
- ExpandedToolDetail,
8
7
  InlineThinkingItem,
9
- InlineToolItem,
10
- LiveDuration,
11
8
  RunGroup
12
- } from "./chunk-JBPWIYTQ.js";
9
+ } from "./chunk-LASW7CYH.js";
13
10
  import {
11
+ ExpandedToolDetail,
12
+ InlineToolItem,
13
+ LiveDuration,
14
14
  ToolCallGroup,
15
15
  ToolCallStep
16
- } from "./chunk-DGW77LD7.js";
17
- import "./chunk-4CLN43XT.js";
18
- import "./chunk-BX6AQMUS.js";
16
+ } from "./chunk-EOGJX2TU.js";
17
+ import "./chunk-ULDNFLIM.js";
19
18
  import "./chunk-AAUNOHVL.js";
20
19
  import "./chunk-52Y3FMFI.js";
21
20
  import "./chunk-GYPQXTJU.js";
package/dist/sdk-hooks.js CHANGED
@@ -5,16 +5,18 @@ import {
5
5
  useSSEStream,
6
6
  useSdkSession,
7
7
  useToolCallStream
8
- } from "./chunk-CMX2I43A.js";
8
+ } from "./chunk-PN3S2MTV.js";
9
9
  import "./chunk-OEX7NZE3.js";
10
10
  import {
11
11
  useAutoScroll,
12
12
  useRunCollapseState,
13
13
  useRunGroups
14
- } from "./chunk-54SQQMMM.js";
15
- import "./chunk-5VPTNXX7.js";
16
- import "./chunk-DGW77LD7.js";
17
- import "./chunk-BX6AQMUS.js";
14
+ } from "./chunk-AZWDI2JG.js";
15
+ import "./chunk-O6NUUCT2.js";
16
+ import "./chunk-EOGJX2TU.js";
17
+ import "./chunk-ULDNFLIM.js";
18
+ import "./chunk-AAUNOHVL.js";
19
+ import "./chunk-ZRVH3WCA.js";
18
20
  import "./chunk-FJBTCTZM.js";
19
21
  import "./chunk-WUQDUBJG.js";
20
22
  import "./chunk-RQHJBTEU.js";
package/dist/utils.js CHANGED
@@ -3,17 +3,15 @@ import {
3
3
  timeAgo
4
4
  } from "./chunk-XGKULLYE.js";
5
5
  import {
6
+ TOOL_CATEGORY_ICONS,
6
7
  formatBytes,
7
8
  formatDuration,
8
9
  formatUptime,
9
- truncateText
10
- } from "./chunk-4CLN43XT.js";
11
- import {
12
- TOOL_CATEGORY_ICONS,
13
10
  getToolCategory,
14
11
  getToolDisplayMetadata,
15
- getToolErrorText
16
- } from "./chunk-BX6AQMUS.js";
12
+ getToolErrorText,
13
+ truncateText
14
+ } from "./chunk-ULDNFLIM.js";
17
15
  import {
18
16
  cn
19
17
  } from "./chunk-RQHJBTEU.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,6 +53,10 @@ export interface InlineToolItemProps {
53
53
  className?: string;
54
54
  contentClassName?: string;
55
55
  actions?: ReactNode;
56
+ /** Override the derived title (default: from `getToolDisplayMetadata`). */
57
+ title?: string;
58
+ /** Override the derived inline description. */
59
+ description?: string;
56
60
  }
57
61
 
58
62
  /**
@@ -68,9 +72,13 @@ export const InlineToolItem = memo(
68
72
  className,
69
73
  contentClassName,
70
74
  actions,
75
+ title: titleOverride,
76
+ description: descriptionOverride,
71
77
  }: InlineToolItemProps) => {
72
78
  const [open, setOpen] = useState(false);
73
79
  const meta = getToolDisplayMetadata(part);
80
+ const title = titleOverride ?? meta.title;
81
+ const description = descriptionOverride ?? meta.description;
74
82
  const { status } = part.state;
75
83
  const errorText = getToolErrorText(part);
76
84
 
@@ -101,8 +109,8 @@ export const InlineToolItem = memo(
101
109
  <button
102
110
  className={cn(
103
111
  "w-full border text-left transition-colors",
104
- "border-border bg-card hover:border-[var(--border-accent-hover)] hover:bg-accent/35",
105
- open && "border-border bg-accent/30",
112
+ "border-[var(--border-subtle)] bg-card/40 hover:border-border hover:bg-accent/25",
113
+ open && "border-border bg-accent/20",
106
114
  shapeClass,
107
115
  className,
108
116
  )}
@@ -127,11 +135,11 @@ export const InlineToolItem = memo(
127
135
  </div>
128
136
 
129
137
  <span className="truncate text-xs font-medium text-foreground">
130
- {meta.title}
138
+ {title}
131
139
  </span>
132
- {meta.description ? (
140
+ {description ? (
133
141
  <span className="hidden truncate text-xs font-mono text-muted-foreground sm:inline">
134
- {meta.description}
142
+ {description}
135
143
  </span>
136
144
  ) : null}
137
145
 
@@ -179,7 +187,7 @@ export const InlineToolItem = memo(
179
187
  </div>
180
188
 
181
189
  <Collapsible.Content className="overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp">
182
- <div className={cn("mt-2 pl-4", contentClassName)}>
190
+ <div className={cn("mt-1.5 ml-2.5 border-l-2 border-primary/15 pl-3.5", contentClassName)}>
183
191
  {renderToolDetail?.(part) ?? <ExpandedToolDetail part={part} />}
184
192
  </div>
185
193
  </Collapsible.Content>
@@ -58,9 +58,9 @@ function AssistantShell({
58
58
  children: ReactNode;
59
59
  }) {
60
60
  return (
61
- <div className="flex gap-3">
62
- <div className="mt-1 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--brand-primary)] text-white">
63
- <Bot className="h-4 w-4" />
61
+ <div className="flex gap-2.5">
62
+ <div className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--brand-primary)] text-white">
63
+ <Bot className="h-3.5 w-3.5" />
64
64
  </div>
65
65
 
66
66
  <div className={ASSISTANT_SHELL}>
@@ -402,7 +402,7 @@ export const RunGroup = memo(
402
402
  <Collapsible.Root open={!collapsed} onOpenChange={() => onToggle()}>
403
403
  <div className="rounded-[28px] border border-[var(--border-subtle)] bg-[var(--bg-card)] shadow-none">
404
404
  {/* Header */}
405
- <div className="flex items-start gap-3 px-4 py-3.5">
405
+ <div className="flex items-start gap-3 px-3 py-2.5">
406
406
  <Collapsible.Trigger asChild>
407
407
  <button
408
408
  className={cn(
@@ -410,13 +410,13 @@ export const RunGroup = memo(
410
410
  "bg-transparent hover:bg-transparent",
411
411
  )}
412
412
  >
413
- <div className="flex items-center gap-2.5">
413
+ <div className="flex items-center gap-2">
414
414
  <div
415
415
  className={cn(
416
- "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--brand-primary)] text-white",
416
+ "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--brand-primary)] text-white",
417
417
  )}
418
418
  >
419
- <Bot className="h-4 w-4" />
419
+ <Bot className="h-3.5 w-3.5" />
420
420
  </div>
421
421
 
422
422
  <span className={cn("text-sm font-semibold", branding.textClass)}>
@@ -473,10 +473,24 @@ export const RunGroup = memo(
473
473
 
474
474
  {/* Expanded content */}
475
475
  <Collapsible.Content className="overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp">
476
- <div className={cn("space-y-3 border-t border-[var(--border-subtle)] px-4 pb-4 pt-3")}>
476
+ <div className={cn("border-t border-[var(--border-subtle)] px-4 pb-4 pt-3")}>
477
477
  {allParts.map(({ part, msgId, index }, partIndex) => {
478
478
  const key = `${msgId}-${index}`;
479
479
 
480
+ // Consecutive (non-OpenUI) tool calls connect into one block —
481
+ // `getToolGroupPosition` already gives them joined radii, so they
482
+ // get no vertical gap; every other transition keeps a normal gap.
483
+ const prev = allParts[partIndex - 1]?.part;
484
+ const connectedTool =
485
+ part.type === "tool" &&
486
+ prev?.type === "tool" &&
487
+ !isOpenUITool(part as ToolPart) &&
488
+ !isOpenUITool(prev as ToolPart);
489
+ const gapClass =
490
+ partIndex === 0 ? "" : connectedTool ? "mt-px" : "mt-3";
491
+
492
+ let node: ReactNode = null;
493
+
480
494
  if (part.type === "tool") {
481
495
  if (isOpenUITool(part as ToolPart)) {
482
496
  const toolPart = part as ToolPart;
@@ -484,11 +498,8 @@ export const RunGroup = memo(
484
498
  const summary = getOpenUISummary(toolPart.state.output);
485
499
 
486
500
  if (toolPart.state.status === "completed" && schema) {
487
- return (
488
- <div
489
- key={key}
490
- className="overflow-hidden rounded-[24px] border border-[var(--border-subtle)] bg-[var(--bg-card)]"
491
- >
501
+ node = (
502
+ <div className="overflow-hidden rounded-[24px] border border-[var(--border-subtle)] bg-[var(--bg-card)]">
492
503
  {summary ? (
493
504
  <div className="border-b border-[var(--border-subtle)] px-4 py-3">
494
505
  <div className="text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
@@ -502,14 +513,9 @@ export const RunGroup = memo(
502
513
  </div>
503
514
  </div>
504
515
  );
505
- }
506
-
507
- if (toolPart.state.status === "running") {
508
- return (
509
- <div
510
- key={key}
511
- className="flex items-center gap-3 rounded-[20px] border border-[var(--border-subtle)] bg-[var(--bg-card)] px-4 py-3 text-sm text-muted-foreground"
512
- >
516
+ } else if (toolPart.state.status === "running") {
517
+ node = (
518
+ <div className="flex items-center gap-3 rounded-[20px] border border-[var(--border-subtle)] bg-[var(--bg-card)] px-4 py-3 text-sm text-muted-foreground">
513
519
  <Loader2 className="h-4 w-4 animate-spin text-primary" />
514
520
  Building view…
515
521
  </div>
@@ -517,47 +523,45 @@ export const RunGroup = memo(
517
523
  }
518
524
  }
519
525
 
520
- return (
521
- <InlineToolItem
522
- key={key}
523
- part={part as ToolPart}
524
- renderToolDetail={renderToolDetail}
525
- groupPosition={getToolGroupPosition(partIndex, allParts)}
526
- actions={renderToolActions?.(part as ToolPart, {
527
- run,
528
- messageId: msgId,
529
- partIndex: index,
530
- })}
531
- />
532
- );
533
- }
534
-
535
- if (part.type === "reasoning") {
536
- return (
526
+ if (node === null) {
527
+ node = (
528
+ <InlineToolItem
529
+ part={part as ToolPart}
530
+ renderToolDetail={renderToolDetail}
531
+ groupPosition={getToolGroupPosition(partIndex, allParts)}
532
+ actions={renderToolActions?.(part as ToolPart, {
533
+ run,
534
+ messageId: msgId,
535
+ partIndex: index,
536
+ })}
537
+ />
538
+ );
539
+ }
540
+ } else if (part.type === "reasoning") {
541
+ node = (
537
542
  <InlineThinkingItem
538
- key={key}
539
543
  part={part as ReasoningPart}
540
544
  defaultOpen={isStreaming}
541
545
  />
542
546
  );
543
- }
544
-
545
- if (
547
+ } else if (
546
548
  part.type === "text" &&
547
549
  !part.synthetic &&
548
550
  part.text.trim()
549
551
  ) {
550
- return (
551
- <div
552
- key={key}
553
- className="px-1 py-1"
554
- >
552
+ node = (
553
+ <div className="px-1 py-1">
555
554
  <Markdown className="tangle-prose text-[15px] leading-7">{part.text}</Markdown>
556
555
  </div>
557
556
  );
558
557
  }
559
558
 
560
- return null;
559
+ if (!node) return null;
560
+ return (
561
+ <div key={key} className={gapClass}>
562
+ {node}
563
+ </div>
564
+ );
561
565
  })}
562
566
  </div>
563
567
  </Collapsible.Content>
@@ -1,26 +1,17 @@
1
1
  /**
2
- * ToolCallStep — renders a single agent tool invocation as a collapsible activity step.
2
+ * ToolCallStep — a single agent tool invocation as a collapsible activity step.
3
3
  *
4
- * Inspired by Conductor's workspace activity feed.
5
- * Each step shows: icon, label, optional output, expandable detail.
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.
6
9
  */
7
10
 
8
- import { useState, type ReactNode } from "react";
9
- import {
10
- Terminal,
11
- FileText,
12
- FileCode,
13
- Search,
14
- CheckCircle,
15
- ChevronRight,
16
- Loader2,
17
- FolderOpen,
18
- Download,
19
- Pencil,
20
- Eye,
21
- } from "lucide-react";
22
- import { cn } from "../lib/utils";
11
+ import { type ReactNode } from "react";
23
12
  import { CodeBlock } from "../markdown/code-block";
13
+ import type { ToolPart, ToolStatus } from "../types/parts";
14
+ import { InlineToolItem } from "./inline-tool-item";
24
15
 
25
16
  export type ToolCallType =
26
17
  | "bash"
@@ -64,52 +55,19 @@ const EXT_LANGUAGE: Record<string, string> = {
64
55
  go: "go",
65
56
  sql: "sql",
66
57
  xml: "xml",
67
- }
58
+ };
68
59
 
69
60
  function inferLanguage(detail?: string, language?: string): string | undefined {
70
- if (language) return language
71
- if (!detail) return undefined
72
- const ext = detail.split(".").pop()?.toLowerCase()
73
- return ext ? EXT_LANGUAGE[ext] : undefined
74
- }
75
-
76
- function isFilePath(detail: string): boolean {
77
- return /[/\\]/.test(detail) || /\.\w{1,6}$/.test(detail)
61
+ if (language) return language;
62
+ if (!detail) return undefined;
63
+ const ext = detail.split(".").pop()?.toLowerCase();
64
+ return ext ? EXT_LANGUAGE[ext] : undefined;
78
65
  }
79
66
 
80
- function FilePathChip({ path }: { path: string }) {
81
- const parts = path.replace(/\\/g, "/").split("/")
82
- const filename = parts.pop() ?? path
83
- const dir = parts.length > 0 ? parts.join("/") + "/" : ""
84
- return (
85
- <div className="flex items-center gap-1.5 rounded-[var(--radius-sm)] border border-border bg-background px-2.5 py-1.5 font-mono text-xs min-w-0">
86
- <FileCode className="h-3.5 w-3.5 shrink-0 text-primary" />
87
- {dir && (
88
- <span className="truncate text-muted-foreground">{dir}</span>
89
- )}
90
- <span className="shrink-0 font-semibold text-foreground">{filename}</span>
91
- </div>
92
- )
93
- }
94
-
95
- const ICONS: Record<ToolCallType, typeof Terminal> = {
96
- bash: Terminal,
97
- read: Eye,
98
- write: FileText,
99
- edit: Pencil,
100
- glob: FolderOpen,
101
- grep: Search,
102
- list: FolderOpen,
103
- download: Download,
104
- inspect: Search,
105
- audit: CheckCircle,
106
- unknown: FileCode,
107
- };
108
-
109
- const STATUS_COLORS: Record<ToolCallStatus, string> = {
110
- running: "text-primary",
111
- success: "text-[var(--code-success)]",
112
- error: "text-[var(--code-error)]",
67
+ const STATUS_MAP: Record<ToolCallStatus, ToolStatus> = {
68
+ running: "running",
69
+ success: "completed",
70
+ error: "error",
113
71
  };
114
72
 
115
73
  export function ToolCallStep({
@@ -122,98 +80,38 @@ export function ToolCallStep({
122
80
  duration,
123
81
  className,
124
82
  }: ToolCallStepProps) {
125
- const [expanded, setExpanded] = useState(false);
126
- const Icon = ICONS[type] || ICONS.unknown;
127
- const hasExpandable = !!(detail || output);
83
+ const part: ToolPart = {
84
+ type: "tool",
85
+ id: `${type}:${label}`,
86
+ tool: type,
87
+ state: {
88
+ status: STATUS_MAP[status],
89
+ input: detail ? { detail } : undefined,
90
+ output,
91
+ time: duration != null ? { start: 0, end: duration } : undefined,
92
+ },
93
+ };
94
+
128
95
  const lang = inferLanguage(detail, language);
129
96
 
130
97
  return (
131
- <div
132
- className={cn(
133
- "group overflow-hidden rounded-[var(--radius-lg)] border bg-card transition-colors",
134
- status === "running" && "border-border",
135
- status === "success" && "border-border hover:border-primary/20",
136
- status === "error" && "border-[var(--surface-danger-border)]",
137
- className,
138
- )}
139
- >
140
- <button
141
- onClick={() => hasExpandable && setExpanded(!expanded)}
142
- disabled={!hasExpandable}
143
- className={cn(
144
- "flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm",
145
- hasExpandable && "cursor-pointer",
146
- )}
147
- >
148
- <div
149
- className={cn(
150
- "flex h-6 w-6 shrink-0 items-center justify-center rounded-[var(--radius-sm)] border",
151
- status === "running" && "border-[var(--border-accent)] bg-[var(--accent-surface-soft)] text-primary",
152
- status === "success" && "border-[var(--surface-success-border)] bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]",
153
- status === "error" && "border-[var(--surface-danger-border)] bg-[var(--surface-danger-bg)] text-[var(--surface-danger-text)]",
154
- )}
155
- >
156
- {status === "running" ? (
157
- <Loader2 className="h-3 w-3 animate-spin shrink-0" />
158
- ) : (
159
- <Icon className={cn("h-3 w-3 shrink-0", STATUS_COLORS[status])} />
160
- )}
161
- </div>
162
-
163
- {/* Label */}
164
- <span className="truncate flex-1 font-sans text-foreground">
165
- {label}
166
- </span>
167
-
168
- <span
169
- className={cn(
170
- "rounded-full border px-2 py-0.5 text-[11px] font-semibold uppercase tracking-[0.06em]",
171
- status === "running" &&
172
- "border-border bg-[var(--accent-surface-soft)] text-primary",
173
- status === "success" &&
174
- "border-[var(--surface-success-border)] bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]",
175
- status === "error" && "border-[var(--surface-danger-border)] bg-[var(--surface-danger-bg)] text-[var(--surface-danger-text)]",
176
- )}
177
- >
178
- {status}
179
- </span>
180
-
181
- {/* Duration */}
182
- {duration !== undefined && status !== "running" && (
183
- <span className="shrink-0 text-xs tabular-nums text-muted-foreground">
184
- {duration < 1000 ? `${duration}ms` : `${(duration / 1000).toFixed(1)}s`}
185
- </span>
186
- )}
187
-
188
- {/* Expand chevron */}
189
- {hasExpandable && (
190
- <ChevronRight
191
- className={cn(
192
- "h-3 w-3 text-muted-foreground transition-transform shrink-0",
193
- expanded && "rotate-90",
194
- )}
195
- />
196
- )}
197
- </button>
198
-
199
- {/* Expandable content */}
200
- {expanded && (detail || output) && (
201
- <div className="space-y-2 border-t border-border bg-muted px-3 py-2.5">
202
- {detail && (
203
- isFilePath(detail)
204
- ? <FilePathChip path={detail} />
205
- : <div className="text-xs font-mono text-muted-foreground">{detail}</div>
206
- )}
207
- {output && (
208
- <CodeBlock
209
- code={output}
210
- language={lang}
211
- className="max-h-72 overflow-auto text-xs"
212
- />
213
- )}
214
- </div>
215
- )}
216
- </div>
98
+ <InlineToolItem
99
+ part={part}
100
+ title={label}
101
+ description={detail}
102
+ className={className}
103
+ renderToolDetail={
104
+ output
105
+ ? () => (
106
+ <CodeBlock
107
+ code={output}
108
+ language={lang}
109
+ className="max-h-72 overflow-auto text-xs"
110
+ />
111
+ )
112
+ : () => null
113
+ }
114
+ />
217
115
  );
218
116
  }
219
117
 
@@ -228,7 +126,7 @@ export interface ToolCallGroupProps {
228
126
 
229
127
  export function ToolCallGroup({ title, children, className }: ToolCallGroupProps) {
230
128
  return (
231
- <div className={cn("my-2 space-y-2", className)}>
129
+ <div className={["my-2 space-y-2", className].filter(Boolean).join(" ")}>
232
130
  {title && (
233
131
  <div className="mb-1 px-1 text-xs font-medium uppercase tracking-wider text-muted-foreground">
234
132
  {title}
@@ -1,45 +0,0 @@
1
- // src/utils/format.ts
2
- function formatDuration(ms) {
3
- if (ms < 1e3) return "<1s";
4
- const seconds = Math.floor(ms / 1e3);
5
- if (seconds < 60) return `${seconds}s`;
6
- const minutes = Math.floor(seconds / 60);
7
- const remaining = seconds % 60;
8
- return remaining > 0 ? `${minutes}m ${remaining}s` : `${minutes}m`;
9
- }
10
- function truncateText(text, max) {
11
- const cleaned = text.replace(/\s+/g, " ").trim();
12
- if (cleaned.length <= max) return cleaned;
13
- return cleaned.slice(0, max).trim() + "...";
14
- }
15
- function formatUptime(ms) {
16
- if (!Number.isFinite(ms) || ms < 0) return "\u2014";
17
- const totalSeconds = Math.floor(ms / 1e3);
18
- if (totalSeconds < 60) return `${totalSeconds}s`;
19
- const minutes = Math.floor(totalSeconds / 60);
20
- const seconds = totalSeconds % 60;
21
- if (minutes < 60) return `${minutes}m ${seconds}s`;
22
- const hours = Math.floor(minutes / 60);
23
- const remMinutes = minutes % 60;
24
- if (hours < 24) return `${hours}h ${remMinutes}m`;
25
- const days = Math.floor(hours / 24);
26
- const remHours = hours % 24;
27
- return `${days}d ${remHours}h`;
28
- }
29
- function formatBytes(bytes) {
30
- if (!Number.isFinite(bytes) || bytes < 0) return "\u2014";
31
- if (bytes < 1024) return `${Math.round(bytes)} B`;
32
- const kb = bytes / 1024;
33
- if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`;
34
- const mb = kb / 1024;
35
- if (mb < 1024) return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
36
- const gb = mb / 1024;
37
- return `${gb < 10 ? gb.toFixed(2) : gb.toFixed(1)} GB`;
38
- }
39
-
40
- export {
41
- formatDuration,
42
- truncateText,
43
- formatUptime,
44
- formatBytes
45
- };