| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783 |
- import map from "lang-map"
- import { DateTime } from "luxon"
- import { For, Show, Match, Switch, type JSX, createMemo, createSignal, type ParentProps } from "solid-js"
- import {
- IconHashtag,
- IconSparkles,
- IconGlobeAlt,
- IconDocument,
- IconPaperClip,
- IconQueueList,
- IconUserCircle,
- IconCommandLine,
- IconCheckCircle,
- IconChevronDown,
- IconChevronRight,
- IconDocumentPlus,
- IconPencilSquare,
- IconRectangleStack,
- IconMagnifyingGlass,
- IconDocumentMagnifyingGlass,
- } from "../icons"
- import { IconMeta, IconRobot, IconOpenAI, IconGemini, IconAnthropic, IconBrain } from "../icons/custom"
- import { ContentCode } from "./content-code"
- import { ContentDiff } from "./content-diff"
- import { ContentText } from "./content-text"
- import { ContentBash } from "./content-bash"
- import { ContentError } from "./content-error"
- import { formatDuration } from "../share/common"
- import { ContentMarkdown } from "./content-markdown"
- import type { MessageV2 } from "opencode/session/message-v2"
- import type { Diagnostic } from "vscode-languageserver-types"
- import styles from "./part.module.css"
- const MIN_DURATION = 2000
- export interface PartProps {
- index: number
- message: MessageV2.Info
- part: MessageV2.Part
- last: boolean
- }
- export function Part(props: PartProps) {
- const [copied, setCopied] = createSignal(false)
- const id = createMemo(() => props.message.id + "-" + props.index)
- return (
- <div
- class={styles.root}
- id={id()}
- data-component="part"
- data-type={props.part.type}
- data-role={props.message.role}
- data-copied={copied() ? true : undefined}
- >
- <div data-component="decoration">
- <div data-slot="anchor" title="Link to this message">
- <a
- href={`#${id()}`}
- onClick={(e) => {
- e.preventDefault()
- const anchor = e.currentTarget
- const hash = anchor.getAttribute("href") || ""
- const { origin, pathname, search } = window.location
- navigator.clipboard
- .writeText(`${origin}${pathname}${search}${hash}`)
- .catch((err) => console.error("Copy failed", err))
- setCopied(true)
- setTimeout(() => setCopied(false), 3000)
- }}
- >
- <Switch>
- <Match when={props.message.role === "user" && props.part.type === "text"}>
- <IconUserCircle width={18} height={18} />
- </Match>
- <Match when={props.message.role === "user" && props.part.type === "file"}>
- <IconPaperClip width={18} height={18} />
- </Match>
- <Match
- when={props.part.type === "step-start" && props.message.role === "assistant" && props.message.modelID}
- >
- {(model) => <ProviderIcon model={model()} size={18} />}
- </Match>
- <Match when={props.part.type === "reasoning" && props.message.role === "assistant"}>
- <IconBrain width={18} height={18} />
- </Match>
- <Match when={props.part.type === "tool" && props.part.tool === "todowrite"}>
- <IconQueueList width={18} height={18} />
- </Match>
- <Match when={props.part.type === "tool" && props.part.tool === "todoread"}>
- <IconQueueList width={18} height={18} />
- </Match>
- <Match when={props.part.type === "tool" && props.part.tool === "bash"}>
- <IconCommandLine width={18} height={18} />
- </Match>
- <Match when={props.part.type === "tool" && props.part.tool === "edit"}>
- <IconPencilSquare width={18} height={18} />
- </Match>
- <Match when={props.part.type === "tool" && props.part.tool === "write"}>
- <IconDocumentPlus width={18} height={18} />
- </Match>
- <Match when={props.part.type === "tool" && props.part.tool === "read"}>
- <IconDocument width={18} height={18} />
- </Match>
- <Match when={props.part.type === "tool" && props.part.tool === "grep"}>
- <IconDocumentMagnifyingGlass width={18} height={18} />
- </Match>
- <Match when={props.part.type === "tool" && props.part.tool === "list"}>
- <IconRectangleStack width={18} height={18} />
- </Match>
- <Match when={props.part.type === "tool" && props.part.tool === "glob"}>
- <IconMagnifyingGlass width={18} height={18} />
- </Match>
- <Match when={props.part.type === "tool" && props.part.tool === "webfetch"}>
- <IconGlobeAlt width={18} height={18} />
- </Match>
- <Match when={props.part.type === "tool" && props.part.tool === "task"}>
- <IconRobot width={18} height={18} />
- </Match>
- <Match when={true}>
- <IconSparkles width={18} height={18} />
- </Match>
- </Switch>
- <IconHashtag width={18} height={18} />
- <IconCheckCircle width={18} height={18} />
- </a>
- <span data-slot="tooltip">Copied!</span>
- </div>
- <div data-slot="bar"></div>
- </div>
- <div data-component="content">
- {props.message.role === "user" && props.part.type === "text" && (
- <div data-component="user-text">
- <ContentText text={props.part.text} expand={props.last} />
- </div>
- )}
- {props.message.role === "assistant" && props.part.type === "text" && (
- <div data-component="assistant-text">
- <div data-component="assistant-text-markdown">
- <ContentMarkdown expand={props.last} text={props.part.text} />
- </div>
- {props.last && props.message.role === "assistant" && props.message.time.completed && (
- <Footer
- title={DateTime.fromMillis(props.message.time.completed).toLocaleString(
- DateTime.DATETIME_FULL_WITH_SECONDS,
- )}
- >
- {DateTime.fromMillis(props.message.time.completed || props.message.time.created).toLocaleString(
- DateTime.DATETIME_MED,
- )}
- {` | ${props.message.modelID}`}
- {props.message.mode && (
- <span style={{ "font-weight": "bold", color: "var(--sl-color-accent)" }}>
- {` | ${props.message.mode}`}
- </span>
- )}
- </Footer>
- )}
- </div>
- )}
- {props.message.role === "assistant" && props.part.type === "reasoning" && (
- <div data-component="assistant-reasoning">
- <div data-component="assistant-reasoning-markdown">
- <ContentMarkdown expand={props.last} text={props.part.text || "Thinking..."} />
- </div>
- </div>
- )}
- {props.message.role === "user" && props.part.type === "file" && (
- <div data-component="attachment">
- <div data-slot="copy">Attachment</div>
- <div data-slot="filename">{props.part.filename}</div>
- </div>
- )}
- {props.part.type === "step-start" && props.message.role === "assistant" && (
- <div data-component="step-start">
- <div data-slot="provider">{props.message.providerID}</div>
- <div data-slot="model">
- {DateTime.fromMillis(props.message.time.completed || props.message.time.created).toLocaleString(
- DateTime.DATETIME_MED,
- )}
- {` | ${props.message.modelID}`}
- {props.message.mode && (
- <span style={{ "font-weight": "bold", color: "var(--sl-color-accent)" }}>
- {` | ${props.message.mode}`}
- </span>
- )}
- </div>
- </div>
- )}
- {props.part.type === "tool" && props.part.state.status === "error" && (
- <div data-component="tool" data-tool="error">
- <ContentError>{formatErrorString(props.part.state.error)}</ContentError>
- <Spacer />
- </div>
- )}
- {props.part.type === "tool" &&
- props.part.state.status === "completed" &&
- props.message.role === "assistant" && (
- <>
- <div data-component="tool" data-tool={props.part.tool}>
- <Switch>
- <Match when={props.part.tool === "grep"}>
- <GrepTool
- message={props.message}
- id={props.part.id}
- tool={props.part.tool}
- state={props.part.state}
- />
- </Match>
- <Match when={props.part.tool === "glob"}>
- <GlobTool
- message={props.message}
- id={props.part.id}
- tool={props.part.tool}
- state={props.part.state}
- />
- </Match>
- <Match when={props.part.tool === "list"}>
- <ListTool
- message={props.message}
- id={props.part.id}
- tool={props.part.tool}
- state={props.part.state}
- />
- </Match>
- <Match when={props.part.tool === "read"}>
- <ReadTool
- message={props.message}
- id={props.part.id}
- tool={props.part.tool}
- state={props.part.state}
- />
- </Match>
- <Match when={props.part.tool === "write"}>
- <WriteTool
- message={props.message}
- id={props.part.id}
- tool={props.part.tool}
- state={props.part.state}
- />
- </Match>
- <Match when={props.part.tool === "edit"}>
- <EditTool
- message={props.message}
- id={props.part.id}
- tool={props.part.tool}
- state={props.part.state}
- />
- </Match>
- <Match when={props.part.tool === "bash"}>
- <BashTool
- id={props.part.id}
- tool={props.part.tool}
- state={props.part.state}
- message={props.message}
- />
- </Match>
- <Match when={props.part.tool === "todowrite"}>
- <TodoWriteTool
- message={props.message}
- id={props.part.id}
- tool={props.part.tool}
- state={props.part.state}
- />
- </Match>
- <Match when={props.part.tool === "webfetch"}>
- <WebFetchTool
- message={props.message}
- id={props.part.id}
- tool={props.part.tool}
- state={props.part.state}
- />
- </Match>
- <Match when={props.part.tool === "task"}>
- <TaskTool
- id={props.part.id}
- tool={props.part.tool}
- message={props.message}
- state={props.part.state}
- />
- </Match>
- <Match when={true}>
- <FallbackTool
- message={props.message}
- id={props.part.id}
- tool={props.part.tool}
- state={props.part.state}
- />
- </Match>
- </Switch>
- </div>
- <ToolFooter
- time={DateTime.fromMillis(props.part.state.time.end)
- .diff(DateTime.fromMillis(props.part.state.time.start))
- .toMillis()}
- />
- </>
- )}
- </div>
- </div>
- )
- }
- type ToolProps = {
- id: MessageV2.ToolPart["id"]
- tool: MessageV2.ToolPart["tool"]
- state: MessageV2.ToolStateCompleted
- message: MessageV2.Assistant
- isLastPart?: boolean
- }
- interface Todo {
- id: string
- content: string
- status: "pending" | "in_progress" | "completed"
- priority: "low" | "medium" | "high"
- }
- function stripWorkingDirectory(filePath?: string, workingDir?: string) {
- if (filePath === undefined || workingDir === undefined) return filePath
- const prefix = workingDir.endsWith("/") ? workingDir : workingDir + "/"
- if (filePath === workingDir) {
- return ""
- }
- if (filePath.startsWith(prefix)) {
- return filePath.slice(prefix.length)
- }
- return filePath
- }
- function getShikiLang(filename: string) {
- const ext = filename.split(".").pop()?.toLowerCase() ?? ""
- const langs = map.languages(ext)
- const type = langs?.[0]?.toLowerCase()
- const overrides: Record<string, string> = {
- conf: "shellscript",
- }
- return type ? (overrides[type] ?? type) : "plaintext"
- }
- function getDiagnostics(diagnosticsByFile: Record<string, Diagnostic[]>, currentFile: string): JSX.Element[] {
- const result: JSX.Element[] = []
- if (diagnosticsByFile === undefined || diagnosticsByFile[currentFile] === undefined) return result
- for (const diags of Object.values(diagnosticsByFile)) {
- for (const d of diags) {
- if (d.severity !== 1) continue
- const line = d.range.start.line + 1
- const column = d.range.start.character + 1
- result.push(
- <pre>
- <span data-color="red" data-marker="label">
- Error
- </span>
- <span data-color="dimmed" data-separator>
- [{line}:{column}]
- </span>
- <span>{d.message}</span>
- </pre>,
- )
- }
- }
- return result
- }
- function formatErrorString(error: string): JSX.Element {
- const errorMarker = "Error: "
- const startsWithError = error.startsWith(errorMarker)
- return startsWithError ? (
- <pre>
- <span data-color="red" data-marker="label" data-separator>
- Error
- </span>
- <span>{error.slice(errorMarker.length)}</span>
- </pre>
- ) : (
- <pre>
- <span data-color="dimmed">{error}</span>
- </pre>
- )
- }
- export function TodoWriteTool(props: ToolProps) {
- const priority: Record<Todo["status"], number> = {
- in_progress: 0,
- pending: 1,
- completed: 2,
- }
- const todos = createMemo(() =>
- ((props.state.input?.todos ?? []) as Todo[]).slice().sort((a, b) => priority[a.status] - priority[b.status]),
- )
- const starting = () => todos().every((t: Todo) => t.status === "pending")
- const finished = () => todos().every((t: Todo) => t.status === "completed")
- return (
- <>
- <div data-component="tool-title">
- <span data-slot="name">
- <Switch fallback="Updating plan">
- <Match when={starting()}>Creating plan</Match>
- <Match when={finished()}>Completing plan</Match>
- </Switch>
- </span>
- </div>
- <Show when={todos().length > 0}>
- <ul data-component="todos">
- <For each={todos()}>
- {(todo) => (
- <li data-slot="item" data-status={todo.status}>
- <span></span>
- {todo.content}
- </li>
- )}
- </For>
- </ul>
- </Show>
- </>
- )
- }
- export function GrepTool(props: ToolProps) {
- return (
- <>
- <div data-component="tool-title">
- <span data-slot="name">Grep</span>
- <span data-slot="target">“{props.state.input.pattern}”</span>
- </div>
- <div data-component="tool-result">
- <Switch>
- <Match when={props.state.metadata?.matches && props.state.metadata?.matches > 0}>
- <ResultsButton
- showCopy={props.state.metadata?.matches === 1 ? "1 match" : `${props.state.metadata?.matches} matches`}
- >
- <ContentText expand compact text={props.state.output} />
- </ResultsButton>
- </Match>
- <Match when={props.state.output}>
- <ContentText expand compact text={props.state.output} data-size="sm" data-color="dimmed" />
- </Match>
- </Switch>
- </div>
- </>
- )
- }
- export function ListTool(props: ToolProps) {
- const path = createMemo(() =>
- props.state.input?.path !== props.message.path.cwd
- ? stripWorkingDirectory(props.state.input?.path, props.message.path.cwd)
- : props.state.input?.path,
- )
- return (
- <>
- <div data-component="tool-title">
- <span data-slot="name">LS</span>
- <span data-slot="target" title={props.state.input?.path}>
- {path()}
- </span>
- </div>
- <div data-component="tool-result">
- <Switch>
- <Match when={props.state.output}>
- <ResultsButton>
- <ContentText expand compact text={props.state.output} />
- </ResultsButton>
- </Match>
- </Switch>
- </div>
- </>
- )
- }
- export function WebFetchTool(props: ToolProps) {
- return (
- <>
- <div data-component="tool-title">
- <span data-slot="name">Fetch</span>
- <span data-slot="target">{props.state.input.url}</span>
- </div>
- <div data-component="tool-result">
- <Switch>
- <Match when={props.state.metadata?.error}>
- <ContentError>{formatErrorString(props.state.output)}</ContentError>
- </Match>
- <Match when={props.state.output}>
- <ResultsButton>
- <ContentCode lang={props.state.input.format || "text"} code={props.state.output} />
- </ResultsButton>
- </Match>
- </Switch>
- </div>
- </>
- )
- }
- export function ReadTool(props: ToolProps) {
- const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd))
- return (
- <>
- <div data-component="tool-title">
- <span data-slot="name">Read</span>
- <span data-slot="target" title={props.state.input?.filePath}>
- {filePath()}
- </span>
- </div>
- <div data-component="tool-result">
- <Switch>
- <Match when={props.state.metadata?.error}>
- <ContentError>{formatErrorString(props.state.output)}</ContentError>
- </Match>
- <Match when={typeof props.state.metadata?.preview === "string"}>
- <ResultsButton showCopy="Show preview" hideCopy="Hide preview">
- <ContentCode lang={getShikiLang(filePath() || "")} code={props.state.metadata?.preview} />
- </ResultsButton>
- </Match>
- <Match when={typeof props.state.metadata?.preview !== "string" && props.state.output}>
- <ResultsButton>
- <ContentText expand compact text={props.state.output} />
- </ResultsButton>
- </Match>
- </Switch>
- </div>
- </>
- )
- }
- export function WriteTool(props: ToolProps) {
- const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd))
- const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath))
- return (
- <>
- <div data-component="tool-title">
- <span data-slot="name">Write</span>
- <span data-slot="target" title={props.state.input?.filePath}>
- {filePath()}
- </span>
- </div>
- <Show when={diagnostics().length > 0}>
- <ContentError>{diagnostics()}</ContentError>
- </Show>
- <div data-component="tool-result">
- <Switch>
- <Match when={props.state.metadata?.error}>
- <ContentError>{formatErrorString(props.state.output)}</ContentError>
- </Match>
- <Match when={props.state.input?.content}>
- <ResultsButton showCopy="Show contents" hideCopy="Hide contents">
- <ContentCode lang={getShikiLang(filePath() || "")} code={props.state.input?.content} />
- </ResultsButton>
- </Match>
- </Switch>
- </div>
- </>
- )
- }
- export function EditTool(props: ToolProps) {
- const filePath = createMemo(() => stripWorkingDirectory(props.state.input.filePath, props.message.path.cwd))
- const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath))
- return (
- <>
- <div data-component="tool-title">
- <span data-slot="name">Edit</span>
- <span data-slot="target" title={props.state.input?.filePath}>
- {filePath()}
- </span>
- </div>
- <div data-component="tool-result">
- <Switch>
- <Match when={props.state.metadata?.error}>
- <ContentError>{formatErrorString(props.state.metadata?.message || "")}</ContentError>
- </Match>
- <Match when={props.state.metadata?.diff}>
- <div data-component="diff">
- <ContentDiff diff={props.state.metadata?.diff} lang={getShikiLang(filePath() || "")} />
- </div>
- </Match>
- </Switch>
- </div>
- <Show when={diagnostics().length > 0}>
- <ContentError>{diagnostics()}</ContentError>
- </Show>
- </>
- )
- }
- export function BashTool(props: ToolProps) {
- return (
- <ContentBash
- command={props.state.input.command}
- output={props.state.metadata?.stdout || ""}
- description={props.state.metadata.description}
- />
- )
- }
- export function GlobTool(props: ToolProps) {
- return (
- <>
- <div data-component="tool-title">
- <span data-slot="name">Glob</span>
- <span data-slot="target">“{props.state.input.pattern}”</span>
- </div>
- <Switch>
- <Match when={props.state.metadata?.count && props.state.metadata?.count > 0}>
- <div data-component="tool-result">
- <ResultsButton
- showCopy={props.state.metadata?.count === 1 ? "1 result" : `${props.state.metadata?.count} results`}
- >
- <ContentText expand compact text={props.state.output} />
- </ResultsButton>
- </div>
- </Match>
- <Match when={props.state.output}>
- <ContentText expand text={props.state.output} data-size="sm" data-color="dimmed" />
- </Match>
- </Switch>
- </>
- )
- }
- interface ResultsButtonProps extends ParentProps {
- showCopy?: string
- hideCopy?: string
- }
- function ResultsButton(props: ResultsButtonProps) {
- const [show, setShow] = createSignal(false)
- return (
- <>
- <button type="button" data-component="button-text" data-more onClick={() => setShow((e) => !e)}>
- <span>{show() ? props.hideCopy || "Hide results" : props.showCopy || "Show results"}</span>
- <span data-slot="icon">
- <Show when={show()} fallback={<IconChevronRight width={11} height={11} />}>
- <IconChevronDown width={11} height={11} />
- </Show>
- </span>
- </button>
- <Show when={show()}>{props.children}</Show>
- </>
- )
- }
- export function Spacer() {
- return <div data-component="spacer"></div>
- }
- function Footer(props: ParentProps<{ title: string }>) {
- return (
- <div data-component="content-footer" title={props.title}>
- {props.children}
- </div>
- )
- }
- function ToolFooter(props: { time: number }) {
- return props.time > MIN_DURATION && <Footer title={`${props.time}ms`}>{formatDuration(props.time)}</Footer>
- }
- function TaskTool(props: ToolProps) {
- return (
- <>
- <div data-component="tool-title">
- <span data-slot="name">Task</span>
- <span data-slot="target">{props.state.input.description}</span>
- </div>
- <div data-component="tool-input">“{props.state.input.prompt}”</div>
- <ResultsButton showCopy="Show output" hideCopy="Hide output">
- <div data-component="tool-output">
- <ContentMarkdown expand text={props.state.output} />
- </div>
- </ResultsButton>
- </>
- )
- }
- export function FallbackTool(props: ToolProps) {
- return (
- <>
- <div data-component="tool-title">
- <span data-slot="name">{props.tool}</span>
- </div>
- <div data-component="tool-args">
- <For each={flattenToolArgs(props.state.input)}>
- {(arg) => (
- <>
- <div></div>
- <div>{arg[0]}</div>
- <div>{arg[1]}</div>
- </>
- )}
- </For>
- </div>
- <Switch>
- <Match when={props.state.output}>
- <div data-component="tool-result">
- <ResultsButton>
- <ContentText expand compact text={props.state.output} data-size="sm" data-color="dimmed" />
- </ResultsButton>
- </div>
- </Match>
- </Switch>
- </>
- )
- }
- // Converts nested objects/arrays into [path, value] pairs.
- // E.g. {a:{b:{c:1}}, d:[{e:2}, 3]} => [["a.b.c",1], ["d[0].e",2], ["d[1]",3]]
- function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> {
- const entries: Array<[string, any]> = []
- for (const [key, value] of Object.entries(obj)) {
- const path = prefix ? `${prefix}.${key}` : key
- if (value !== null && typeof value === "object") {
- if (Array.isArray(value)) {
- value.forEach((item, index) => {
- const arrayPath = `${path}[${index}]`
- if (item !== null && typeof item === "object") {
- entries.push(...flattenToolArgs(item, arrayPath))
- } else {
- entries.push([arrayPath, item])
- }
- })
- } else {
- entries.push(...flattenToolArgs(value, path))
- }
- } else {
- entries.push([path, value])
- }
- }
- return entries
- }
- function getProvider(model: string) {
- const lowerModel = model.toLowerCase()
- if (/claude|anthropic/.test(lowerModel)) return "anthropic"
- if (/gpt|o[1-4]|codex|openai/.test(lowerModel)) return "openai"
- if (/gemini|palm|bard|google/.test(lowerModel)) return "gemini"
- if (/llama|meta/.test(lowerModel)) return "meta"
- return "any"
- }
- export function ProviderIcon(props: { model: string; size?: number }) {
- const provider = getProvider(props.model)
- const size = props.size || 16
- return (
- <Switch fallback={<IconSparkles width={size} height={size} />}>
- <Match when={provider === "openai"}>
- <IconOpenAI width={size} height={size} />
- </Match>
- <Match when={provider === "anthropic"}>
- <IconAnthropic width={size} height={size} />
- </Match>
- <Match when={provider === "gemini"}>
- <IconGemini width={size} height={size} />
- </Match>
- <Match when={provider === "meta"}>
- <IconMeta width={size} height={size} />
- </Match>
- </Switch>
- )
- }
|