mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-23 09:04:47 +00:00
Animation Smorgasbord (#15637)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
onMount,
|
||||
Show,
|
||||
Switch,
|
||||
onCleanup,
|
||||
@@ -47,6 +48,42 @@ import { checksum } from "@opencode-ai/util/encode"
|
||||
import { Tooltip } from "./tooltip"
|
||||
import { IconButton } from "./icon-button"
|
||||
import { TextShimmer } from "./text-shimmer"
|
||||
import { AnimatedCountList } from "./tool-count-summary"
|
||||
import { ToolStatusTitle } from "./tool-status-title"
|
||||
import { animate } from "motion"
|
||||
|
||||
function ShellSubmessage(props: { text: string; animate?: boolean }) {
|
||||
let widthRef: HTMLSpanElement | undefined
|
||||
let valueRef: HTMLSpanElement | undefined
|
||||
|
||||
onMount(() => {
|
||||
if (!props.animate) return
|
||||
requestAnimationFrame(() => {
|
||||
if (widthRef) {
|
||||
animate(widthRef, { width: "auto" }, { type: "spring", visualDuration: 0.25, bounce: 0 })
|
||||
}
|
||||
if (valueRef) {
|
||||
animate(valueRef, { opacity: 1, filter: "blur(0px)" }, { duration: 0.32, ease: [0.16, 1, 0.3, 1] })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<span data-component="shell-submessage">
|
||||
<span ref={widthRef} data-slot="shell-submessage-width" style={{ width: props.animate ? "0px" : undefined }}>
|
||||
<span data-slot="basic-tool-tool-subtitle">
|
||||
<span
|
||||
ref={valueRef}
|
||||
data-slot="shell-submessage-value"
|
||||
style={props.animate ? { opacity: 0, filter: "blur(2px)" } : undefined}
|
||||
>
|
||||
{props.text}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
interface Diagnostic {
|
||||
range: {
|
||||
@@ -272,6 +309,102 @@ function list<T>(value: T[] | undefined | null, fallback: T[]) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
function same<T>(a: readonly T[] | undefined, b: readonly T[] | undefined) {
|
||||
if (a === b) return true
|
||||
if (!a || !b) return false
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((x, i) => x === b[i])
|
||||
}
|
||||
|
||||
type PartRef = {
|
||||
messageID: string
|
||||
partID: string
|
||||
}
|
||||
|
||||
type PartGroup =
|
||||
| {
|
||||
key: string
|
||||
type: "part"
|
||||
ref: PartRef
|
||||
}
|
||||
| {
|
||||
key: string
|
||||
type: "context"
|
||||
refs: PartRef[]
|
||||
}
|
||||
|
||||
function sameRef(a: PartRef, b: PartRef) {
|
||||
return a.messageID === b.messageID && a.partID === b.partID
|
||||
}
|
||||
|
||||
function sameGroup(a: PartGroup, b: PartGroup) {
|
||||
if (a === b) return true
|
||||
if (a.key !== b.key) return false
|
||||
if (a.type !== b.type) return false
|
||||
if (a.type === "part") {
|
||||
if (b.type !== "part") return false
|
||||
return sameRef(a.ref, b.ref)
|
||||
}
|
||||
if (b.type !== "context") return false
|
||||
if (a.refs.length !== b.refs.length) return false
|
||||
return a.refs.every((ref, i) => sameRef(ref, b.refs[i]!))
|
||||
}
|
||||
|
||||
function sameGroups(a: readonly PartGroup[] | undefined, b: readonly PartGroup[] | undefined) {
|
||||
if (a === b) return true
|
||||
if (!a || !b) return false
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((item, i) => sameGroup(item, b[i]!))
|
||||
}
|
||||
|
||||
function groupParts(parts: { messageID: string; part: PartType }[]) {
|
||||
const result: PartGroup[] = []
|
||||
let start = -1
|
||||
|
||||
const flush = (end: number) => {
|
||||
if (start < 0) return
|
||||
const first = parts[start]
|
||||
const last = parts[end]
|
||||
if (!first || !last) {
|
||||
start = -1
|
||||
return
|
||||
}
|
||||
result.push({
|
||||
key: `context:${first.part.id}`,
|
||||
type: "context",
|
||||
refs: parts.slice(start, end + 1).map((item) => ({
|
||||
messageID: item.messageID,
|
||||
partID: item.part.id,
|
||||
})),
|
||||
})
|
||||
start = -1
|
||||
}
|
||||
|
||||
parts.forEach((item, index) => {
|
||||
if (isContextGroupTool(item.part)) {
|
||||
if (start < 0) start = index
|
||||
return
|
||||
}
|
||||
|
||||
flush(index - 1)
|
||||
result.push({
|
||||
key: `part:${item.messageID}:${item.part.id}`,
|
||||
type: "part",
|
||||
ref: {
|
||||
messageID: item.messageID,
|
||||
partID: item.part.id,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
flush(parts.length - 1)
|
||||
return result
|
||||
}
|
||||
|
||||
function partByID(parts: readonly PartType[], partID: string) {
|
||||
return parts.find((part) => part.id === partID)
|
||||
}
|
||||
|
||||
function renderable(part: PartType, showReasoningSummaries = true) {
|
||||
if (part.type === "tool") {
|
||||
if (HIDDEN_TOOLS.has(part.tool)) return false
|
||||
@@ -304,98 +437,68 @@ export function AssistantParts(props: {
|
||||
}) {
|
||||
const data = useData()
|
||||
const emptyParts: PartType[] = []
|
||||
const emptyTools: ToolPart[] = []
|
||||
|
||||
const grouped = createMemo(() => {
|
||||
const keys: string[] = []
|
||||
const items: Record<
|
||||
string,
|
||||
{ type: "part"; part: PartType; message: AssistantMessage } | { type: "context"; parts: ToolPart[] }
|
||||
> = {}
|
||||
const push = (
|
||||
key: string,
|
||||
item: { type: "part"; part: PartType; message: AssistantMessage } | { type: "context"; parts: ToolPart[] },
|
||||
) => {
|
||||
keys.push(key)
|
||||
items[key] = item
|
||||
}
|
||||
const grouped = createMemo(
|
||||
() =>
|
||||
groupParts(
|
||||
props.messages.flatMap((message) =>
|
||||
list(data.store.part?.[message.id], emptyParts)
|
||||
.filter((part) => renderable(part, props.showReasoningSummaries ?? true))
|
||||
.map((part) => ({
|
||||
messageID: message.id,
|
||||
part,
|
||||
})),
|
||||
),
|
||||
),
|
||||
[] as PartGroup[],
|
||||
{ equals: sameGroups },
|
||||
)
|
||||
|
||||
const parts = props.messages.flatMap((message) =>
|
||||
list(data.store.part?.[message.id], emptyParts)
|
||||
.filter((part) => renderable(part, props.showReasoningSummaries ?? true))
|
||||
.map((part) => ({ message, part })),
|
||||
)
|
||||
|
||||
let start = -1
|
||||
|
||||
const flush = (end: number) => {
|
||||
if (start < 0) return
|
||||
const first = parts[start]
|
||||
const last = parts[end]
|
||||
if (!first || !last) {
|
||||
start = -1
|
||||
return
|
||||
}
|
||||
push(`context:${first.part.id}`, {
|
||||
type: "context",
|
||||
parts: parts
|
||||
.slice(start, end + 1)
|
||||
.map((x) => x.part)
|
||||
.filter((part): part is ToolPart => isContextGroupTool(part)),
|
||||
})
|
||||
start = -1
|
||||
}
|
||||
|
||||
parts.forEach((item, index) => {
|
||||
if (isContextGroupTool(item.part)) {
|
||||
if (start < 0) start = index
|
||||
return
|
||||
}
|
||||
|
||||
flush(index - 1)
|
||||
push(`part:${item.message.id}:${item.part.id}`, { type: "part", part: item.part, message: item.message })
|
||||
})
|
||||
|
||||
flush(parts.length - 1)
|
||||
|
||||
return { keys, items }
|
||||
})
|
||||
|
||||
const last = createMemo(() => grouped().keys.at(-1))
|
||||
const last = createMemo(() => grouped().at(-1)?.key)
|
||||
|
||||
return (
|
||||
<For each={grouped().keys}>
|
||||
{(key) => {
|
||||
const item = createMemo(() => grouped().items[key])
|
||||
const ctx = createMemo(() => {
|
||||
const value = item()
|
||||
if (!value) return
|
||||
if (value.type !== "context") return
|
||||
return value
|
||||
})
|
||||
const part = createMemo(() => {
|
||||
const value = item()
|
||||
if (!value) return
|
||||
if (value.type !== "part") return
|
||||
return value
|
||||
})
|
||||
const tail = createMemo(() => last() === key)
|
||||
<For each={grouped()}>
|
||||
{(entry) => {
|
||||
if (entry.type === "context") {
|
||||
const parts = createMemo(
|
||||
() =>
|
||||
entry.refs
|
||||
.map((ref) => partByID(list(data.store.part?.[ref.messageID], emptyParts), ref.partID))
|
||||
.filter((part): part is ToolPart => !!part && isContextGroupTool(part)),
|
||||
emptyTools,
|
||||
{ equals: same },
|
||||
)
|
||||
const busy = createMemo(() => props.working && last() === entry.key)
|
||||
|
||||
return (
|
||||
<Show when={parts().length > 0}>
|
||||
<ContextToolGroup parts={parts()} busy={busy()} />
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
const message = createMemo(() => props.messages.find((item) => item.id === entry.ref.messageID))
|
||||
const part = createMemo(() =>
|
||||
partByID(list(data.store.part?.[entry.ref.messageID], emptyParts), entry.ref.partID),
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={ctx()}>
|
||||
{(entry) => <ContextToolGroup parts={entry().parts} busy={props.working && tail()} />}
|
||||
</Show>
|
||||
<Show when={part()}>
|
||||
{(entry) => (
|
||||
<Part
|
||||
part={entry().part}
|
||||
message={entry().message}
|
||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||
turnDurationMs={props.turnDurationMs}
|
||||
defaultOpen={partDefaultOpen(entry().part, props.shellToolDefaultOpen, props.editToolDefaultOpen)}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
<Show when={message()}>
|
||||
{(message) => (
|
||||
<Show when={part()}>
|
||||
{(part) => (
|
||||
<Part
|
||||
part={part()}
|
||||
message={message()}
|
||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||
turnDurationMs={props.turnDurationMs}
|
||||
defaultOpen={partDefaultOpen(part(), props.shellToolDefaultOpen, props.editToolDefaultOpen)}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
@@ -469,23 +572,11 @@ function contextToolTrigger(part: ToolPart, i18n: ReturnType<typeof useI18n>) {
|
||||
}
|
||||
}
|
||||
|
||||
function contextToolSummary(parts: ToolPart[], i18n: ReturnType<typeof useI18n>) {
|
||||
function contextToolSummary(parts: ToolPart[]) {
|
||||
const read = parts.filter((part) => part.tool === "read").length
|
||||
const search = parts.filter((part) => part.tool === "glob" || part.tool === "grep").length
|
||||
const list = parts.filter((part) => part.tool === "list").length
|
||||
return [
|
||||
read
|
||||
? i18n.t(read === 1 ? "ui.messagePart.context.read.one" : "ui.messagePart.context.read.other", { count: read })
|
||||
: undefined,
|
||||
search
|
||||
? i18n.t(search === 1 ? "ui.messagePart.context.search.one" : "ui.messagePart.context.search.other", {
|
||||
count: search,
|
||||
})
|
||||
: undefined,
|
||||
list
|
||||
? i18n.t(list === 1 ? "ui.messagePart.context.list.one" : "ui.messagePart.context.list.other", { count: list })
|
||||
: undefined,
|
||||
].filter((value): value is string => !!value)
|
||||
return { read, search, list }
|
||||
}
|
||||
|
||||
export function registerPartComponent(type: string, component: PartComponent) {
|
||||
@@ -525,78 +616,49 @@ export function AssistantMessageDisplay(props: {
|
||||
showAssistantCopyPartID?: string | null
|
||||
showReasoningSummaries?: boolean
|
||||
}) {
|
||||
const grouped = createMemo(() => {
|
||||
const keys: string[] = []
|
||||
const items: Record<string, { type: "part"; part: PartType } | { type: "context"; parts: ToolPart[] }> = {}
|
||||
const push = (key: string, item: { type: "part"; part: PartType } | { type: "context"; parts: ToolPart[] }) => {
|
||||
keys.push(key)
|
||||
items[key] = item
|
||||
}
|
||||
|
||||
const parts = props.parts
|
||||
let start = -1
|
||||
|
||||
const flush = (end: number) => {
|
||||
if (start < 0) return
|
||||
const first = parts[start]
|
||||
const last = parts[end]
|
||||
if (!first || !last) {
|
||||
start = -1
|
||||
return
|
||||
}
|
||||
push(`context:${first.id}`, {
|
||||
type: "context",
|
||||
parts: parts.slice(start, end + 1).filter((part): part is ToolPart => isContextGroupTool(part)),
|
||||
})
|
||||
start = -1
|
||||
}
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
if (!renderable(part, props.showReasoningSummaries ?? true)) return
|
||||
|
||||
if (isContextGroupTool(part)) {
|
||||
if (start < 0) start = index
|
||||
return
|
||||
}
|
||||
|
||||
flush(index - 1)
|
||||
push(`part:${part.id}`, { type: "part", part })
|
||||
})
|
||||
|
||||
flush(parts.length - 1)
|
||||
|
||||
return { keys, items }
|
||||
})
|
||||
const emptyTools: ToolPart[] = []
|
||||
const grouped = createMemo(
|
||||
() =>
|
||||
groupParts(
|
||||
props.parts
|
||||
.filter((part) => renderable(part, props.showReasoningSummaries ?? true))
|
||||
.map((part) => ({
|
||||
messageID: props.message.id,
|
||||
part,
|
||||
})),
|
||||
),
|
||||
[] as PartGroup[],
|
||||
{ equals: sameGroups },
|
||||
)
|
||||
|
||||
return (
|
||||
<For each={grouped().keys}>
|
||||
{(key) => {
|
||||
const item = createMemo(() => grouped().items[key])
|
||||
const ctx = createMemo(() => {
|
||||
const value = item()
|
||||
if (!value) return
|
||||
if (value.type !== "context") return
|
||||
return value
|
||||
})
|
||||
const part = createMemo(() => {
|
||||
const value = item()
|
||||
if (!value) return
|
||||
if (value.type !== "part") return
|
||||
return value
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<Show when={ctx()}>{(entry) => <ContextToolGroup parts={entry().parts} />}</Show>
|
||||
<Show when={part()}>
|
||||
{(entry) => (
|
||||
<Part
|
||||
part={entry().part}
|
||||
message={props.message}
|
||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||
/>
|
||||
)}
|
||||
<For each={grouped()}>
|
||||
{(entry) => {
|
||||
if (entry.type === "context") {
|
||||
const parts = createMemo(
|
||||
() =>
|
||||
entry.refs
|
||||
.map((ref) => partByID(props.parts, ref.partID))
|
||||
.filter((part): part is ToolPart => !!part && isContextGroupTool(part)),
|
||||
emptyTools,
|
||||
{ equals: same },
|
||||
)
|
||||
|
||||
return (
|
||||
<Show when={parts().length > 0}>
|
||||
<ContextToolGroup parts={parts()} />
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const part = createMemo(() => partByID(props.parts, entry.ref.partID))
|
||||
|
||||
return (
|
||||
<Show when={part()}>
|
||||
{(part) => (
|
||||
<Part part={part()} message={props.message} showAssistantCopyPartID={props.showAssistantCopyPartID} />
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
@@ -610,33 +672,53 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
|
||||
() =>
|
||||
!!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
|
||||
)
|
||||
const summary = createMemo(() => contextToolSummary(props.parts, i18n))
|
||||
const details = createMemo(() => summary().join(", "))
|
||||
const summary = createMemo(() => contextToolSummary(props.parts))
|
||||
|
||||
return (
|
||||
<Collapsible open={open()} onOpenChange={setOpen} variant="ghost">
|
||||
<Collapsible.Trigger>
|
||||
<div data-component="context-tool-group-trigger">
|
||||
<Show
|
||||
when={pending()}
|
||||
fallback={
|
||||
<span data-slot="context-tool-group-title">
|
||||
<span data-slot="context-tool-group-label">{i18n.t("ui.sessionTurn.status.gatheredContext")}</span>
|
||||
<Show when={details().length}>
|
||||
<span data-slot="context-tool-group-summary">{details()}</span>
|
||||
</Show>
|
||||
</span>
|
||||
}
|
||||
<span
|
||||
data-slot="context-tool-group-title"
|
||||
class="min-w-0 flex items-center gap-2 text-14-medium text-text-strong"
|
||||
>
|
||||
<span data-slot="context-tool-group-title">
|
||||
<span data-slot="context-tool-group-label">
|
||||
<TextShimmer text={i18n.t("ui.sessionTurn.status.gatheringContext")} />
|
||||
</span>
|
||||
<Show when={details().length}>
|
||||
<span data-slot="context-tool-group-summary">{details()}</span>
|
||||
</Show>
|
||||
<span data-slot="context-tool-group-label" class="shrink-0">
|
||||
<ToolStatusTitle
|
||||
active={pending()}
|
||||
activeText={i18n.t("ui.sessionTurn.status.gatheringContext")}
|
||||
doneText={i18n.t("ui.sessionTurn.status.gatheredContext")}
|
||||
split={false}
|
||||
/>
|
||||
</span>
|
||||
</Show>
|
||||
<span
|
||||
data-slot="context-tool-group-summary"
|
||||
class="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap font-normal text-text-base"
|
||||
>
|
||||
<AnimatedCountList
|
||||
items={[
|
||||
{
|
||||
key: "read",
|
||||
count: summary().read,
|
||||
one: i18n.t("ui.messagePart.context.read.one"),
|
||||
other: i18n.t("ui.messagePart.context.read.other"),
|
||||
},
|
||||
{
|
||||
key: "search",
|
||||
count: summary().search,
|
||||
one: i18n.t("ui.messagePart.context.search.one"),
|
||||
other: i18n.t("ui.messagePart.context.search.other"),
|
||||
},
|
||||
{
|
||||
key: "list",
|
||||
count: summary().list,
|
||||
one: i18n.t("ui.messagePart.context.list.one"),
|
||||
other: i18n.t("ui.messagePart.context.list.other"),
|
||||
},
|
||||
]}
|
||||
fallback=""
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<Collapsible.Arrow />
|
||||
</div>
|
||||
</Collapsible.Trigger>
|
||||
@@ -654,9 +736,7 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
|
||||
<div data-slot="basic-tool-tool-info-structured">
|
||||
<div data-slot="basic-tool-tool-info-main">
|
||||
<span data-slot="basic-tool-tool-title">
|
||||
<Show when={running} fallback={trigger.title}>
|
||||
<TextShimmer text={trigger.title} />
|
||||
</Show>
|
||||
<TextShimmer text={trigger.title} active={running} />
|
||||
</span>
|
||||
<Show when={!running && trigger.subtitle}>
|
||||
<span data-slot="basic-tool-tool-subtitle">{trigger.subtitle}</span>
|
||||
@@ -1319,9 +1399,7 @@ ToolRegistry.register({
|
||||
<div data-slot="basic-tool-tool-info-structured">
|
||||
<div data-slot="basic-tool-tool-info-main">
|
||||
<span data-slot="basic-tool-tool-title">
|
||||
<Show when={pending()} fallback={i18n.t("ui.tool.webfetch")}>
|
||||
<TextShimmer text={i18n.t("ui.tool.webfetch")} />
|
||||
</Show>
|
||||
<TextShimmer text={i18n.t("ui.tool.webfetch")} active={pending()} />
|
||||
</span>
|
||||
<Show when={!pending() && url()}>
|
||||
<a
|
||||
@@ -1436,6 +1514,8 @@ ToolRegistry.register({
|
||||
name: "bash",
|
||||
render(props) {
|
||||
const i18n = useI18n()
|
||||
const pending = () => props.status === "pending" || props.status === "running"
|
||||
const sawPending = pending()
|
||||
const text = createMemo(() => {
|
||||
const cmd = props.input.command ?? props.metadata.command ?? ""
|
||||
const out = stripAnsi(props.output || props.metadata.output || "")
|
||||
@@ -1455,10 +1535,18 @@ ToolRegistry.register({
|
||||
<BasicTool
|
||||
{...props}
|
||||
icon="console"
|
||||
trigger={{
|
||||
title: i18n.t("ui.tool.shell"),
|
||||
subtitle: props.input.description,
|
||||
}}
|
||||
trigger={
|
||||
<div data-slot="basic-tool-tool-info-structured">
|
||||
<div data-slot="basic-tool-tool-info-main">
|
||||
<span data-slot="basic-tool-tool-title">
|
||||
<TextShimmer text={i18n.t("ui.tool.shell")} active={pending()} />
|
||||
</span>
|
||||
<Show when={!pending() && props.input.description}>
|
||||
<ShellSubmessage text={props.input.description} animate={sawPending} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div data-component="bash-output">
|
||||
<div data-slot="bash-copy">
|
||||
@@ -1508,9 +1596,7 @@ ToolRegistry.register({
|
||||
<div data-slot="message-part-title-area">
|
||||
<div data-slot="message-part-title">
|
||||
<span data-slot="message-part-title-text">
|
||||
<Show when={pending()} fallback={i18n.t("ui.messagePart.title.edit")}>
|
||||
<TextShimmer text={i18n.t("ui.messagePart.title.edit")} />
|
||||
</Show>
|
||||
<TextShimmer text={i18n.t("ui.messagePart.title.edit")} active={pending()} />
|
||||
</span>
|
||||
<Show when={!pending()}>
|
||||
<span data-slot="message-part-title-filename">{filename()}</span>
|
||||
@@ -1580,9 +1666,7 @@ ToolRegistry.register({
|
||||
<div data-slot="message-part-title-area">
|
||||
<div data-slot="message-part-title">
|
||||
<span data-slot="message-part-title-text">
|
||||
<Show when={pending()} fallback={i18n.t("ui.messagePart.title.write")}>
|
||||
<TextShimmer text={i18n.t("ui.messagePart.title.write")} />
|
||||
</Show>
|
||||
<TextShimmer text={i18n.t("ui.messagePart.title.write")} active={pending()} />
|
||||
</span>
|
||||
<Show when={!pending()}>
|
||||
<span data-slot="message-part-title-filename">{filename()}</span>
|
||||
@@ -1774,9 +1858,7 @@ ToolRegistry.register({
|
||||
<div data-slot="message-part-title-area">
|
||||
<div data-slot="message-part-title">
|
||||
<span data-slot="message-part-title-text">
|
||||
<Show when={pending()} fallback={i18n.t("ui.tool.patch")}>
|
||||
<TextShimmer text={i18n.t("ui.tool.patch")} />
|
||||
</Show>
|
||||
<TextShimmer text={i18n.t("ui.tool.patch")} active={pending()} />
|
||||
</span>
|
||||
<Show when={!pending()}>
|
||||
<span data-slot="message-part-title-filename">{getFilename(file().relativePath)}</span>
|
||||
|
||||
Reference in New Issue
Block a user