|
|
@@ -1,4 +1,4 @@
|
|
|
-import React, { memo, useEffect, useMemo, useRef, useState } from "react"
|
|
|
+import React, { memo, useMemo, useRef, useState } from "react"
|
|
|
import { useWindowSize } from "react-use"
|
|
|
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
|
|
import prettyBytes from "pretty-bytes"
|
|
|
@@ -44,267 +44,87 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|
|
const { t } = useTranslation()
|
|
|
const { apiConfiguration, currentTaskItem } = useExtensionState()
|
|
|
const { selectedModelInfo } = useMemo(() => normalizeApiConfiguration(apiConfiguration), [apiConfiguration])
|
|
|
- const [isTaskExpanded, setIsTaskExpanded] = useState(true)
|
|
|
- const [isTextExpanded, setIsTextExpanded] = useState(false)
|
|
|
- const [showSeeMore, setShowSeeMore] = useState(false)
|
|
|
+ const [isTaskExpanded, setIsTaskExpanded] = useState(false)
|
|
|
+
|
|
|
const textContainerRef = useRef<HTMLDivElement>(null)
|
|
|
const textRef = useRef<HTMLDivElement>(null)
|
|
|
const contextWindow = selectedModelInfo?.contextWindow || 1
|
|
|
|
|
|
- /*
|
|
|
- When dealing with event listeners in React components that depend on state
|
|
|
- variables, we face a challenge. We want our listener to always use the most
|
|
|
- up-to-date version of a callback function that relies on current state, but
|
|
|
- we don't want to constantly add and remove event listeners as that function
|
|
|
- updates. This scenario often arises with resize listeners or other window
|
|
|
- events. Simply adding the listener in a useEffect with an empty dependency
|
|
|
- array risks using stale state, while including the callback in the
|
|
|
- dependencies can lead to unnecessary re-registrations of the listener. There
|
|
|
- are react hook libraries that provide a elegant solution to this problem by
|
|
|
- utilizing the useRef hook to maintain a reference to the latest callback
|
|
|
- function without triggering re-renders or effect re-runs. This approach
|
|
|
- ensures that our event listener always has access to the most current state
|
|
|
- while minimizing performance overhead and potential memory leaks from
|
|
|
- multiple listener registrations.
|
|
|
-
|
|
|
- Sources
|
|
|
- - https://usehooks-ts.com/react-hook/use-event-listener
|
|
|
- - https://streamich.github.io/react-use/?path=/story/sensors-useevent--docs
|
|
|
- - https://github.com/streamich/react-use/blob/master/src/useEvent.ts
|
|
|
- - https://stackoverflow.com/questions/55565444/how-to-register-event-with-useeffect-hooks
|
|
|
-
|
|
|
- Before:
|
|
|
-
|
|
|
- const updateMaxHeight = useCallback(() => {
|
|
|
- if (isExpanded && textContainerRef.current) {
|
|
|
- const maxHeight = window.innerHeight * (3 / 5)
|
|
|
- textContainerRef.current.style.maxHeight = `${maxHeight}px`
|
|
|
- }
|
|
|
- }, [isExpanded])
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- updateMaxHeight()
|
|
|
- }, [isExpanded, updateMaxHeight])
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- window.removeEventListener("resize", updateMaxHeight)
|
|
|
- window.addEventListener("resize", updateMaxHeight)
|
|
|
- return () => {
|
|
|
- window.removeEventListener("resize", updateMaxHeight)
|
|
|
- }
|
|
|
- }, [updateMaxHeight])
|
|
|
-
|
|
|
- After:
|
|
|
- */
|
|
|
-
|
|
|
- const { height: windowHeight, width: windowWidth } = useWindowSize()
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- if (isTextExpanded && textContainerRef.current) {
|
|
|
- const maxHeight = windowHeight * (1 / 2)
|
|
|
- textContainerRef.current.style.maxHeight = `${maxHeight}px`
|
|
|
- }
|
|
|
- }, [isTextExpanded, windowHeight])
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- if (textRef.current && textContainerRef.current) {
|
|
|
- let textContainerHeight = textContainerRef.current.clientHeight
|
|
|
- if (!textContainerHeight) {
|
|
|
- textContainerHeight = textContainerRef.current.getBoundingClientRect().height
|
|
|
- }
|
|
|
- const isOverflowing = textRef.current.scrollHeight > textContainerHeight
|
|
|
- // necessary to show see more button again if user resizes window to expand and then back to collapse
|
|
|
- if (!isOverflowing) {
|
|
|
- setIsTextExpanded(false)
|
|
|
- }
|
|
|
- setShowSeeMore(isOverflowing)
|
|
|
- }
|
|
|
- }, [task.text, windowWidth])
|
|
|
-
|
|
|
- const isCostAvailable = useMemo(() => {
|
|
|
- return totalCost !== null && totalCost !== undefined && totalCost > 0 && !isNaN(totalCost)
|
|
|
- }, [totalCost])
|
|
|
+ const { width: windowWidth } = useWindowSize()
|
|
|
|
|
|
const shouldShowPromptCacheInfo = doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter"
|
|
|
|
|
|
return (
|
|
|
- <div style={{ padding: "10px 13px 10px 13px" }}>
|
|
|
+ <div className="py-[10px] px-[13px]">
|
|
|
<div
|
|
|
- style={{
|
|
|
- backgroundColor: "var(--vscode-badge-background)",
|
|
|
- color: "var(--vscode-badge-foreground)",
|
|
|
- borderRadius: "3px",
|
|
|
- padding: "9px 10px 9px 14px",
|
|
|
- display: "flex",
|
|
|
- flexDirection: "column",
|
|
|
- gap: 6,
|
|
|
- position: "relative",
|
|
|
- zIndex: 1,
|
|
|
- }}>
|
|
|
- <div
|
|
|
- style={{
|
|
|
- display: "flex",
|
|
|
- justifyContent: "space-between",
|
|
|
- alignItems: "center",
|
|
|
- }}>
|
|
|
+ className={`rounded p-[10px] flex flex-col gap-[6px] relative z-1 outline hover:outline-vscode-badge-foreground hover:text-vscode-badge-foreground transition-color duration-500 ${!!isTaskExpanded ? "outline-vscode-badge-foreground text-vscode-badge-foreground" : "outline-vscode-badge-foreground/80 text-vscode-badge-foreground/80"}`}>
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
<div
|
|
|
- style={{
|
|
|
- display: "flex",
|
|
|
- alignItems: "center",
|
|
|
- cursor: "pointer",
|
|
|
- marginLeft: -2,
|
|
|
- userSelect: "none",
|
|
|
- WebkitUserSelect: "none",
|
|
|
- MozUserSelect: "none",
|
|
|
- msUserSelect: "none",
|
|
|
- flexGrow: 1,
|
|
|
- minWidth: 0, // This allows the div to shrink below its content size
|
|
|
- }}
|
|
|
+ className="flex items-center cursor-pointer -ml-0.5 select-none grow min-w-0"
|
|
|
onClick={() => setIsTaskExpanded(!isTaskExpanded)}>
|
|
|
- <div style={{ display: "flex", alignItems: "center", flexShrink: 0 }}>
|
|
|
+ <div className="flex items-center shrink-0">
|
|
|
<span className={`codicon codicon-chevron-${isTaskExpanded ? "down" : "right"}`}></span>
|
|
|
</div>
|
|
|
- <div
|
|
|
- style={{
|
|
|
- marginLeft: 6,
|
|
|
- whiteSpace: "nowrap",
|
|
|
- overflow: "hidden",
|
|
|
- textOverflow: "ellipsis",
|
|
|
- flexGrow: 1,
|
|
|
- minWidth: 0, // This allows the div to shrink below its content size
|
|
|
- }}>
|
|
|
- <span style={{ fontWeight: "bold" }}>
|
|
|
+ <div className="ml-1.5 whitespace-nowrap overflow-hidden text-ellipsis grow min-w-0">
|
|
|
+ <span className="font-bold">
|
|
|
{t("chat:task.title")}
|
|
|
{!isTaskExpanded && ":"}
|
|
|
</span>
|
|
|
- {!isTaskExpanded && (
|
|
|
- <span style={{ marginLeft: 4 }}>{highlightMentions(task.text, false)}</span>
|
|
|
- )}
|
|
|
+ {!isTaskExpanded && <span className="ml-1">{highlightMentions(task.text, false)}</span>}
|
|
|
</div>
|
|
|
</div>
|
|
|
- {!isTaskExpanded && isCostAvailable && (
|
|
|
- <div
|
|
|
- style={{
|
|
|
- marginLeft: 10,
|
|
|
- backgroundColor: "color-mix(in srgb, var(--vscode-badge-foreground) 70%, transparent)",
|
|
|
- color: "var(--vscode-badge-background)",
|
|
|
- padding: "2px 4px",
|
|
|
- borderRadius: "500px",
|
|
|
- fontSize: "11px",
|
|
|
- fontWeight: 500,
|
|
|
- display: "inline-block",
|
|
|
- flexShrink: 0,
|
|
|
- }}>
|
|
|
- ${totalCost?.toFixed(4)}
|
|
|
- </div>
|
|
|
- )}
|
|
|
+
|
|
|
<VSCodeButton
|
|
|
appearance="icon"
|
|
|
onClick={onClose}
|
|
|
- style={{ marginLeft: 6, flexShrink: 0, color: "var(--vscode-badge-foreground)" }}
|
|
|
+ className="ml-1.5 shrink-0 text-vscode-badge-foreground"
|
|
|
title={t("chat:task.closeAndStart")}>
|
|
|
<span className="codicon codicon-close"></span>
|
|
|
</VSCodeButton>
|
|
|
</div>
|
|
|
+ {/* Collapsed state: Track context and cost if we have any */}
|
|
|
+ {!isTaskExpanded && contextWindow > 0 && (
|
|
|
+ <div className={`w-full flex flex-row gap-1 h-auto`}>
|
|
|
+ <ContextWindowProgress
|
|
|
+ contextWindow={contextWindow}
|
|
|
+ contextTokens={contextTokens || 0}
|
|
|
+ maxTokens={getMaxTokensForModel(selectedModelInfo, apiConfiguration)}
|
|
|
+ />
|
|
|
+ {!!totalCost && (
|
|
|
+ <div className="ml-2.5 bg-vscode-editor-foreground text-vscode-editor-background py-0.5 px-1 rounded-full text-[11px] font-medium inline-block shrink-0">
|
|
|
+ ${totalCost?.toFixed(2)}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {/* Expanded state: Show task text and images */}
|
|
|
{isTaskExpanded && (
|
|
|
<>
|
|
|
<div
|
|
|
ref={textContainerRef}
|
|
|
- style={{
|
|
|
- marginTop: -2,
|
|
|
- fontSize: "var(--vscode-font-size)",
|
|
|
- overflowY: isTextExpanded ? "auto" : "hidden",
|
|
|
- wordBreak: "break-word",
|
|
|
- overflowWrap: "anywhere",
|
|
|
- position: "relative",
|
|
|
- }}>
|
|
|
+ className="-mt-0.5 text-vscode-font-size overflow-y-auto break-words break-anywhere relative">
|
|
|
<div
|
|
|
ref={textRef}
|
|
|
+ className="overflow-auto max-h-80 whitespace-pre-wrap break-words break-anywhere"
|
|
|
style={{
|
|
|
display: "-webkit-box",
|
|
|
- WebkitLineClamp: isTextExpanded ? "unset" : 3,
|
|
|
+ WebkitLineClamp: "unset",
|
|
|
WebkitBoxOrient: "vertical",
|
|
|
- overflow: "hidden",
|
|
|
- whiteSpace: "pre-wrap",
|
|
|
- wordBreak: "break-word",
|
|
|
- overflowWrap: "anywhere",
|
|
|
}}>
|
|
|
{highlightMentions(task.text, false)}
|
|
|
</div>
|
|
|
- {!isTextExpanded && showSeeMore && (
|
|
|
- <div
|
|
|
- style={{
|
|
|
- position: "absolute",
|
|
|
- right: 0,
|
|
|
- bottom: 0,
|
|
|
- display: "flex",
|
|
|
- alignItems: "center",
|
|
|
- }}>
|
|
|
- <div
|
|
|
- style={{
|
|
|
- width: 30,
|
|
|
- height: "1.2em",
|
|
|
- background:
|
|
|
- "linear-gradient(to right, transparent, var(--vscode-badge-background))",
|
|
|
- }}
|
|
|
- />
|
|
|
- <div
|
|
|
- style={{
|
|
|
- cursor: "pointer",
|
|
|
- color: "var(--vscode-badge-foreground)",
|
|
|
- fontSize: "11px",
|
|
|
- paddingRight: 8,
|
|
|
- paddingLeft: 4,
|
|
|
- backgroundColor: "var(--vscode-badge-background)",
|
|
|
- }}
|
|
|
- onClick={() => setIsTextExpanded(!isTextExpanded)}>
|
|
|
- {t("chat:task.seeMore")}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
</div>
|
|
|
- {isTextExpanded && showSeeMore && (
|
|
|
- <div
|
|
|
- style={{
|
|
|
- cursor: "pointer",
|
|
|
- color: "var(--vscode-badge-foreground)",
|
|
|
- fontSize: "11px",
|
|
|
- marginLeft: "auto",
|
|
|
- textAlign: "right",
|
|
|
- paddingRight: 8,
|
|
|
- }}
|
|
|
- onClick={() => setIsTextExpanded(!isTextExpanded)}>
|
|
|
- {t("chat:task.seeLess")}
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
{task.images && task.images.length > 0 && <Thumbnails images={task.images} />}
|
|
|
|
|
|
- <div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
|
|
- <div className="flex justify-between items-center h-[20px]">
|
|
|
- <div style={{ display: "flex", alignItems: "center", gap: "4px", flexWrap: "wrap" }}>
|
|
|
- <span style={{ fontWeight: "bold" }}>{t("chat:task.tokens")}</span>
|
|
|
- <span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
|
|
|
- <i
|
|
|
- className="codicon codicon-arrow-up"
|
|
|
- style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-2px" }}
|
|
|
- />
|
|
|
- {formatLargeNumber(tokensIn || 0)}
|
|
|
- </span>
|
|
|
- <span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
|
|
|
- <i
|
|
|
- className="codicon codicon-arrow-down"
|
|
|
- style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-2px" }}
|
|
|
- />
|
|
|
- {formatLargeNumber(tokensOut || 0)}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- {!isCostAvailable && <TaskActions item={currentTaskItem} />}
|
|
|
- </div>
|
|
|
-
|
|
|
+ <div className="flex flex-col gap-1">
|
|
|
{isTaskExpanded && contextWindow > 0 && (
|
|
|
<div
|
|
|
className={`w-full flex ${windowWidth < 400 ? "flex-col" : "flex-row"} gap-1 h-auto`}>
|
|
|
+ <div className="flex items-center gap-1 flex-shrink-0">
|
|
|
+ <span className="font-bold" data-testid="context-window-label">
|
|
|
+ {t("chat:task.contextWindow")}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
<ContextWindowProgress
|
|
|
contextWindow={contextWindow}
|
|
|
contextTokens={contextTokens || 0}
|
|
|
@@ -312,32 +132,40 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|
|
/>
|
|
|
</div>
|
|
|
)}
|
|
|
+ <div className="flex justify-between items-center h-[20px]">
|
|
|
+ <div className="flex items-center gap-1 flex-wrap">
|
|
|
+ <span className="font-bold">{t("chat:task.tokens")}</span>
|
|
|
+ <span className="flex items-center gap-[3px]">
|
|
|
+ <i className="codicon codicon-arrow-up text-xs font-bold -mb-0.5" />
|
|
|
+ {formatLargeNumber(tokensIn || 0)}
|
|
|
+ </span>
|
|
|
+ <span className="flex items-center gap-[3px]">
|
|
|
+ <i className="codicon codicon-arrow-down text-xs font-bold -mb-0.5" />
|
|
|
+ {formatLargeNumber(tokensOut || 0)}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ {!totalCost && <TaskActions item={currentTaskItem} />}
|
|
|
+ </div>
|
|
|
|
|
|
{shouldShowPromptCacheInfo && (cacheReads !== undefined || cacheWrites !== undefined) && (
|
|
|
<div className="flex items-center gap-1 flex-wrap h-[20px]">
|
|
|
- <span style={{ fontWeight: "bold" }}>{t("chat:task.cache")}</span>
|
|
|
+ <span className="font-bold">{t("chat:task.cache")}</span>
|
|
|
<span className="flex items-center gap-1">
|
|
|
- <i
|
|
|
- className="codicon codicon-database"
|
|
|
- style={{ fontSize: "12px", fontWeight: "bold" }}
|
|
|
- />
|
|
|
- +{formatLargeNumber(cacheWrites || 0)}
|
|
|
+ <i className="codicon codicon-database text-xs font-bold" />+
|
|
|
+ {formatLargeNumber(cacheWrites || 0)}
|
|
|
</span>
|
|
|
<span className="flex items-center gap-1">
|
|
|
- <i
|
|
|
- className="codicon codicon-arrow-right"
|
|
|
- style={{ fontSize: "12px", fontWeight: "bold" }}
|
|
|
- />
|
|
|
+ <i className="codicon codicon-arrow-right text-xs font-bold" />
|
|
|
{formatLargeNumber(cacheReads || 0)}
|
|
|
</span>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
- {isCostAvailable && (
|
|
|
+ {!!totalCost && (
|
|
|
<div className="flex justify-between items-center h-[20px]">
|
|
|
<div className="flex items-center gap-1">
|
|
|
<span className="font-bold">{t("chat:task.apiCost")}</span>
|
|
|
- <span>${totalCost?.toFixed(4)}</span>
|
|
|
+ <span>${totalCost?.toFixed(2)}</span>
|
|
|
</div>
|
|
|
<TaskActions item={currentTaskItem} />
|
|
|
</div>
|
|
|
@@ -362,8 +190,7 @@ export const highlightMentions = (text?: string, withShadow = true) => {
|
|
|
return (
|
|
|
<span
|
|
|
key={index}
|
|
|
- className={withShadow ? "mention-context-highlight-with-shadow" : "mention-context-highlight"}
|
|
|
- style={{ cursor: "pointer" }}
|
|
|
+ className={`${withShadow ? "mention-context-highlight-with-shadow" : "mention-context-highlight"} cursor-pointer`}
|
|
|
onClick={() => vscode.postMessage({ type: "openMention", text: part })}>
|
|
|
@{part}
|
|
|
</span>
|
|
|
@@ -439,22 +266,12 @@ const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens }: Cont
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
- <div className="flex items-center gap-1 flex-shrink-0">
|
|
|
- <span className="font-bold" data-testid="context-window-label">
|
|
|
- {t("chat:task.contextWindow")}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
<div className="flex items-center gap-2 flex-1 whitespace-nowrap px-2">
|
|
|
<div data-testid="context-tokens-count">{formatLargeNumber(safeContextTokens)}</div>
|
|
|
<div className="flex-1 relative">
|
|
|
{/* Invisible overlay for hover area */}
|
|
|
<div
|
|
|
- className="absolute w-full cursor-pointer"
|
|
|
- style={{
|
|
|
- height: "16px",
|
|
|
- top: "-7px",
|
|
|
- zIndex: 5,
|
|
|
- }}
|
|
|
+ className="absolute w-full cursor-pointer h-4 -top-[7px] z-5"
|
|
|
title={t("chat:tokenProgress.availableSpace", { amount: formatLargeNumber(availableSize) })}
|
|
|
data-testid="context-available-space"
|
|
|
/>
|
|
|
@@ -465,13 +282,7 @@ const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens }: Cont
|
|
|
<div className="relative h-full" style={{ width: `${currentPercent}%` }}>
|
|
|
{/* Invisible overlay for current tokens section */}
|
|
|
<div
|
|
|
- className="absolute cursor-pointer"
|
|
|
- style={{
|
|
|
- height: "16px",
|
|
|
- top: "-7px",
|
|
|
- width: "100%",
|
|
|
- zIndex: 6,
|
|
|
- }}
|
|
|
+ className="absolute cursor-pointer h-4 -top-[7px] w-full z-6"
|
|
|
title={t("chat:tokenProgress.tokensUsed", {
|
|
|
used: formatLargeNumber(safeContextTokens),
|
|
|
total: formatLargeNumber(safeContextWindow),
|
|
|
@@ -479,37 +290,21 @@ const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens }: Cont
|
|
|
data-testid="context-tokens-used"
|
|
|
/>
|
|
|
{/* Current tokens used - darkest */}
|
|
|
- <div
|
|
|
- className="h-full w-full bg-[var(--vscode-badge-foreground)]"
|
|
|
- style={{
|
|
|
- transition: "width 0.3s ease-out",
|
|
|
- }}
|
|
|
- />
|
|
|
+ <div className="h-full w-full bg-[var(--vscode-badge-foreground)] transition-width duration-300 ease-out" />
|
|
|
</div>
|
|
|
|
|
|
{/* Container for reserved tokens */}
|
|
|
<div className="relative h-full" style={{ width: `${reservedPercent}%` }}>
|
|
|
{/* Invisible overlay for reserved section */}
|
|
|
<div
|
|
|
- className="absolute cursor-pointer"
|
|
|
- style={{
|
|
|
- height: "16px",
|
|
|
- top: "-7px",
|
|
|
- width: "100%",
|
|
|
- zIndex: 6,
|
|
|
- }}
|
|
|
+ className="absolute cursor-pointer h-4 -top-[7px] w-full z-6"
|
|
|
title={t("chat:tokenProgress.reservedForResponse", {
|
|
|
amount: formatLargeNumber(reservedForOutput),
|
|
|
})}
|
|
|
data-testid="context-reserved-tokens"
|
|
|
/>
|
|
|
{/* Reserved for output section - medium gray */}
|
|
|
- <div
|
|
|
- className="h-full w-full bg-[color-mix(in_srgb,var(--vscode-badge-foreground)_30%,transparent)]"
|
|
|
- style={{
|
|
|
- transition: "width 0.3s ease-out",
|
|
|
- }}
|
|
|
- />
|
|
|
+ <div className="h-full w-full bg-[color-mix(in_srgb,var(--vscode-badge-foreground)_30%,transparent)] transition-width duration-300 ease-out" />
|
|
|
</div>
|
|
|
|
|
|
{/* Empty section (if any) */}
|
|
|
@@ -517,13 +312,7 @@ const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens }: Cont
|
|
|
<div className="relative h-full" style={{ width: `${availablePercent}%` }}>
|
|
|
{/* Invisible overlay for available space */}
|
|
|
<div
|
|
|
- className="absolute cursor-pointer"
|
|
|
- style={{
|
|
|
- height: "16px",
|
|
|
- top: "-7px",
|
|
|
- width: "100%",
|
|
|
- zIndex: 6,
|
|
|
- }}
|
|
|
+ className="absolute cursor-pointer h-4 -top-[7px] w-full z-6"
|
|
|
title={t("chat:tokenProgress.availableSpace", {
|
|
|
amount: formatLargeNumber(availableSize),
|
|
|
})}
|