diff options
| author | Adam <[email protected]> | 2026-02-09 11:34:35 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-09 11:34:35 -0600 |
| commit | dc53086c1e73d43d3a28fc4cdf161e83d09b1877 (patch) | |
| tree | 45a1d0e38de958d0886a5120b2806b21db74145b /packages/web/src/components/share | |
| parent | f74c0339cc6315f7e7743e26b7eab47ce026c239 (diff) | |
| download | opencode-dc53086c1e73d43d3a28fc4cdf161e83d09b1877.tar.gz opencode-dc53086c1e73d43d3a28fc4cdf161e83d09b1877.zip | |
wip(docs): i18n (#12681)
Diffstat (limited to 'packages/web/src/components/share')
| -rw-r--r-- | packages/web/src/components/share/common.tsx | 71 | ||||
| -rw-r--r-- | packages/web/src/components/share/content-bash.tsx | 5 | ||||
| -rw-r--r-- | packages/web/src/components/share/content-code.tsx | 2 | ||||
| -rw-r--r-- | packages/web/src/components/share/content-error.tsx | 5 | ||||
| -rw-r--r-- | packages/web/src/components/share/content-markdown.tsx | 7 | ||||
| -rw-r--r-- | packages/web/src/components/share/content-text.tsx | 5 | ||||
| -rw-r--r-- | packages/web/src/components/share/copy-button.tsx | 10 | ||||
| -rw-r--r-- | packages/web/src/components/share/part.tsx | 154 |
8 files changed, 177 insertions, 82 deletions
diff --git a/packages/web/src/components/share/common.tsx b/packages/web/src/components/share/common.tsx index cab2dbdb0..7ca4daa6a 100644 --- a/packages/web/src/components/share/common.tsx +++ b/packages/web/src/components/share/common.tsx @@ -1,16 +1,55 @@ -import { createSignal, onCleanup, splitProps } from "solid-js" +import { createContext, createSignal, onCleanup, splitProps, useContext } from "solid-js" import type { JSX } from "solid-js/jsx-runtime" import { IconCheckCircle, IconHashtag } from "../icons" +export type ShareMessages = { locale: string } & Record<string, string> + +const shareContext = createContext<ShareMessages>() + +export function ShareI18nProvider(props: { messages: ShareMessages; children: JSX.Element }) { + return <shareContext.Provider value={props.messages}>{props.children}</shareContext.Provider> +} + +export function useShareMessages() { + const value = useContext(shareContext) + if (value) { + return value + } + throw new Error("ShareI18nProvider is required") +} + +export function normalizeLocale(locale: string) { + return locale === "root" ? "en" : locale +} + +export function formatNumber(value: number, locale: string) { + return new Intl.NumberFormat(normalizeLocale(locale)).format(value) +} + +export function formatCurrency(value: number, locale: string) { + return new Intl.NumberFormat(normalizeLocale(locale), { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value) +} + +export function formatCount(value: number, locale: string, singular: string, plural: string) { + const unit = value === 1 ? singular : plural + return `${formatNumber(value, locale)} ${unit}` +} + interface AnchorProps extends JSX.HTMLAttributes<HTMLDivElement> { id: string } export function AnchorIcon(props: AnchorProps) { const [local, rest] = splitProps(props, ["id", "children"]) const [copied, setCopied] = createSignal(false) + const messages = useShareMessages() return ( - <div {...rest} data-element-anchor title="Link to this message" data-status={copied() ? "copied" : ""}> + <div {...rest} data-element-anchor title={messages.link_to_message} data-status={copied() ? "copied" : ""}> <a href={`#${local.id}`} onClick={(e) => { @@ -32,7 +71,7 @@ export function AnchorIcon(props: AnchorProps) { <IconHashtag width={18} height={18} /> <IconCheckCircle width={18} height={18} /> </a> - <span data-element-tooltip>Copied!</span> + <span data-element-tooltip>{messages.copied}</span> </div> ) } @@ -59,19 +98,33 @@ export function createOverflow() { } } -export function formatDuration(ms: number): string { +export function formatDuration(ms: number, locale: string): string { + const normalized = normalizeLocale(locale) const ONE_SECOND = 1000 const ONE_MINUTE = 60 * ONE_SECOND if (ms >= ONE_MINUTE) { - const minutes = Math.floor(ms / ONE_MINUTE) - return minutes === 1 ? `1min` : `${minutes}mins` + return new Intl.NumberFormat(normalized, { + style: "unit", + unit: "minute", + unitDisplay: "narrow", + maximumFractionDigits: 0, + }).format(Math.floor(ms / ONE_MINUTE)) } if (ms >= ONE_SECOND) { - const seconds = Math.floor(ms / ONE_SECOND) - return `${seconds}s` + return new Intl.NumberFormat(normalized, { + style: "unit", + unit: "second", + unitDisplay: "narrow", + maximumFractionDigits: 0, + }).format(Math.floor(ms / ONE_SECOND)) } - return `${ms}ms` + return new Intl.NumberFormat(normalized, { + style: "unit", + unit: "millisecond", + unitDisplay: "narrow", + maximumFractionDigits: 0, + }).format(ms) } diff --git a/packages/web/src/components/share/content-bash.tsx b/packages/web/src/components/share/content-bash.tsx index 5ccd95c0b..f8130ecc6 100644 --- a/packages/web/src/components/share/content-bash.tsx +++ b/packages/web/src/components/share/content-bash.tsx @@ -1,6 +1,6 @@ import style from "./content-bash.module.css" import { createResource, createSignal } from "solid-js" -import { createOverflow } from "./common" +import { createOverflow, useShareMessages } from "./common" import { codeToHtml } from "shiki" interface Props { @@ -11,6 +11,7 @@ interface Props { } export function ContentBash(props: Props) { + const messages = useShareMessages() const [commandHtml] = createResource( () => props.command, async (command) => { @@ -59,7 +60,7 @@ export function ContentBash(props: Props) { data-slot="expand-button" onClick={() => setExpanded((e) => !e)} > - {expanded() ? "Show less" : "Show more"} + {expanded() ? messages.show_less : messages.show_more} </button> )} </div> diff --git a/packages/web/src/components/share/content-code.tsx b/packages/web/src/components/share/content-code.tsx index 2f383b8be..297828602 100644 --- a/packages/web/src/components/share/content-code.tsx +++ b/packages/web/src/components/share/content-code.tsx @@ -1,6 +1,5 @@ import { codeToHtml, bundledLanguages } from "shiki" import { createResource, Suspense } from "solid-js" -import { transformerNotationDiff } from "@shikijs/transformers" import style from "./content-code.module.css" interface Props { @@ -20,7 +19,6 @@ export function ContentCode(props: Props) { light: "github-light", dark: "github-dark", }, - transformers: [transformerNotationDiff()], })) as string }, ) diff --git a/packages/web/src/components/share/content-error.tsx b/packages/web/src/components/share/content-error.tsx index 1e8cbeaad..0f8588997 100644 --- a/packages/web/src/components/share/content-error.tsx +++ b/packages/web/src/components/share/content-error.tsx @@ -1,6 +1,6 @@ import style from "./content-error.module.css" import { type JSX, createSignal } from "solid-js" -import { createOverflow } from "./common" +import { createOverflow, useShareMessages } from "./common" interface Props extends JSX.HTMLAttributes<HTMLDivElement> { expand?: boolean @@ -8,6 +8,7 @@ interface Props extends JSX.HTMLAttributes<HTMLDivElement> { export function ContentError(props: Props) { const [expanded, setExpanded] = createSignal(false) const overflow = createOverflow() + const messages = useShareMessages() return ( <div class={style.root} data-expanded={expanded() || props.expand === true ? true : undefined}> @@ -16,7 +17,7 @@ export function ContentError(props: Props) { </div> {((!props.expand && overflow.status) || expanded()) && ( <button type="button" data-element-button-text onClick={() => setExpanded((e) => !e)}> - {expanded() ? "Show less" : "Show more"} + {expanded() ? messages.show_less : messages.show_more} </button> )} </div> diff --git a/packages/web/src/components/share/content-markdown.tsx b/packages/web/src/components/share/content-markdown.tsx index b9b1d5dcb..10a06bf5e 100644 --- a/packages/web/src/components/share/content-markdown.tsx +++ b/packages/web/src/components/share/content-markdown.tsx @@ -1,10 +1,9 @@ import { marked } from "marked" import { codeToHtml } from "shiki" import markedShiki from "marked-shiki" -import { createOverflow } from "./common" +import { createOverflow, useShareMessages } from "./common" import { CopyButton } from "./copy-button" import { createResource, createSignal } from "solid-js" -import { transformerNotationDiff } from "@shikijs/transformers" import style from "./content-markdown.module.css" const markedWithShiki = marked.use( @@ -24,7 +23,6 @@ const markedWithShiki = marked.use( light: "github-light", dark: "github-dark", }, - transformers: [transformerNotationDiff()], }) }, }), @@ -44,6 +42,7 @@ export function ContentMarkdown(props: Props) { ) const [expanded, setExpanded] = createSignal(false) const overflow = createOverflow() + const messages = useShareMessages() return ( <div @@ -60,7 +59,7 @@ export function ContentMarkdown(props: Props) { data-slot="expand-button" onClick={() => setExpanded((e) => !e)} > - {expanded() ? "Show less" : "Show more"} + {expanded() ? messages.show_less : messages.show_more} </button> )} <CopyButton text={props.text} /> diff --git a/packages/web/src/components/share/content-text.tsx b/packages/web/src/components/share/content-text.tsx index 5db12a537..b549b74a4 100644 --- a/packages/web/src/components/share/content-text.tsx +++ b/packages/web/src/components/share/content-text.tsx @@ -1,6 +1,6 @@ import style from "./content-text.module.css" import { createSignal } from "solid-js" -import { createOverflow } from "./common" +import { createOverflow, useShareMessages } from "./common" import { CopyButton } from "./copy-button" interface Props { @@ -11,6 +11,7 @@ interface Props { export function ContentText(props: Props) { const [expanded, setExpanded] = createSignal(false) const overflow = createOverflow() + const messages = useShareMessages() return ( <div @@ -28,7 +29,7 @@ export function ContentText(props: Props) { data-slot="expand-button" onClick={() => setExpanded((e) => !e)} > - {expanded() ? "Show less" : "Show more"} + {expanded() ? messages.show_less : messages.show_more} </button> )} <CopyButton text={props.text} /> diff --git a/packages/web/src/components/share/copy-button.tsx b/packages/web/src/components/share/copy-button.tsx index 892d5553f..6c5097a99 100644 --- a/packages/web/src/components/share/copy-button.tsx +++ b/packages/web/src/components/share/copy-button.tsx @@ -1,5 +1,6 @@ import { createSignal } from "solid-js" import { IconClipboard, IconCheckCircle } from "../icons" +import { useShareMessages } from "./common" import styles from "./copy-button.module.css" interface CopyButtonProps { @@ -8,6 +9,7 @@ interface CopyButtonProps { export function CopyButton(props: CopyButtonProps) { const [copied, setCopied] = createSignal(false) + const messages = useShareMessages() function handleCopyClick() { if (props.text) { @@ -20,7 +22,13 @@ export function CopyButton(props: CopyButtonProps) { return ( <div data-component="copy-button" class={styles.root}> - <button type="button" onClick={handleCopyClick} data-copied={copied() ? true : undefined}> + <button + type="button" + onClick={handleCopyClick} + data-copied={copied() ? true : undefined} + aria-label={copied() ? messages.copied : messages.copy} + title={copied() ? messages.copied : messages.copy} + > {copied() ? <IconCheckCircle width={16} height={16} /> : <IconClipboard width={16} height={16} />} </button> </div> diff --git a/packages/web/src/components/share/part.tsx b/packages/web/src/components/share/part.tsx index f7a6a9304..45bd97fe3 100644 --- a/packages/web/src/components/share/part.tsx +++ b/packages/web/src/components/share/part.tsx @@ -25,7 +25,7 @@ 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 { formatCount, formatDuration, formatNumber, normalizeLocale, useShareMessages } from "../share/common" import { ContentMarkdown } from "./content-markdown" import type { MessageV2 } from "opencode/session/message-v2" import type { Diagnostic } from "vscode-languageserver-types" @@ -44,6 +44,7 @@ export interface PartProps { export function Part(props: PartProps) { const [copied, setCopied] = createSignal(false) const id = createMemo(() => props.message.id + "-" + props.index) + const messages = useShareMessages() return ( <div @@ -55,7 +56,7 @@ export function Part(props: PartProps) { data-copied={copied() ? true : undefined} > <div data-component="decoration"> - <div data-slot="anchor" title="Link to this message"> + <div data-slot="anchor" title={messages.link_to_message}> <a href={`#${id()}`} onClick={(e) => { @@ -126,7 +127,7 @@ export function Part(props: PartProps) { <IconHashtag width={18} height={18} /> <IconCheckCircle width={18} height={18} /> </a> - <span data-slot="tooltip">Copied!</span> + <span data-slot="tooltip">{messages.copied}</span> </div> <div data-slot="bar"></div> </div> @@ -143,11 +144,13 @@ export function Part(props: PartProps) { </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, - )} + title={DateTime.fromMillis(props.message.time.completed) + .setLocale(normalizeLocale(messages.locale)) + .toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)} > - {DateTime.fromMillis(props.message.time.completed).toLocaleString(DateTime.DATETIME_MED)} + {DateTime.fromMillis(props.message.time.completed) + .setLocale(normalizeLocale(messages.locale)) + .toLocaleString(DateTime.DATETIME_MED)} </Footer> )} </div> @@ -155,13 +158,13 @@ export function Part(props: PartProps) { {props.message.role === "assistant" && props.part.type === "reasoning" && ( <div data-component="tool"> <div data-component="tool-title"> - <span data-slot="name">Thinking</span> + <span data-slot="name">{messages.thinking}</span> </div> <Show when={props.part.text}> <div data-component="assistant-reasoning"> - <ResultsButton showCopy="Show details" hideCopy="Hide details"> + <ResultsButton showCopy={messages.show_details} hideCopy={messages.hide_details}> <div data-component="assistant-reasoning-markdown"> - <ContentMarkdown expand text={props.part.text || "Thinking..."} /> + <ContentMarkdown expand text={props.part.text || messages.thinking_pending} /> </div> </ResultsButton> </div> @@ -170,13 +173,7 @@ export function Part(props: PartProps) { )} {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.message.role === "user" && props.part.type === "file" && ( - <div data-component="attachment"> - <div data-slot="copy">Attachment</div> + <div data-slot="copy">{messages.attachment}</div> <div data-slot="filename">{props.part.filename}</div> </div> )} @@ -188,7 +185,7 @@ export function Part(props: PartProps) { )} {props.part.type === "tool" && props.part.state.status === "error" && ( <div data-component="tool" data-tool="error"> - <ContentError>{formatErrorString(props.part.state.error)}</ContentError> + <ContentError>{formatErrorString(props.part.state.error, messages.error)}</ContentError> <Spacer /> </div> )} @@ -343,43 +340,45 @@ function getShikiLang(filename: string) { return type ? (overrides[type] ?? type) : "plaintext" } -function getDiagnostics(diagnosticsByFile: Record<string, Diagnostic[]>, currentFile: string): JSX.Element[] { +function getDiagnostics( + diagnosticsByFile: Record<string, Diagnostic[]>, + currentFile: string, + label: 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>, - ) - } + for (const d of diagnosticsByFile[currentFile]) { + 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"> + {label} + </span> + <span data-color="dimmed" data-separator> + [{line}:{column}] + </span> + <span>{d.message}</span> + </pre>, + ) } return result } -function formatErrorString(error: string): JSX.Element { +function formatErrorString(error: string, label: string): JSX.Element { const errorMarker = "Error: " const startsWithError = error.startsWith(errorMarker) return startsWithError ? ( <pre> <span data-color="red" data-marker="label" data-separator> - Error + {label} </span> <span>{error.slice(errorMarker.length)}</span> </pre> @@ -391,6 +390,7 @@ function formatErrorString(error: string): JSX.Element { } export function TodoWriteTool(props: ToolProps) { + const messages = useShareMessages() const priority: Record<Todo["status"], number> = { in_progress: 0, pending: 1, @@ -406,9 +406,9 @@ export function TodoWriteTool(props: ToolProps) { <> <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 fallback={messages.updating_plan}> + <Match when={starting()}>{messages.creating_plan}</Match> + <Match when={finished()}>{messages.completing_plan}</Match> </Switch> </span> </div> @@ -429,6 +429,8 @@ export function TodoWriteTool(props: ToolProps) { } export function GrepTool(props: ToolProps) { + const messages = useShareMessages() + return ( <> <div data-component="tool-title"> @@ -439,7 +441,12 @@ export function GrepTool(props: ToolProps) { <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`} + showCopy={formatCount( + props.state.metadata?.matches || 0, + messages.locale, + messages.match_one, + messages.match_other, + )} > <ContentText expand compact text={props.state.output} /> </ResultsButton> @@ -482,6 +489,8 @@ export function ListTool(props: ToolProps) { } export function WebFetchTool(props: ToolProps) { + const messages = useShareMessages() + return ( <> <div data-component="tool-title"> @@ -491,7 +500,7 @@ export function WebFetchTool(props: ToolProps) { <div data-component="tool-result"> <Switch> <Match when={props.state.metadata?.error}> - <ContentError>{formatErrorString(props.state.output)}</ContentError> + <ContentError>{formatErrorString(props.state.output, messages.error)}</ContentError> </Match> <Match when={props.state.output}> <ResultsButton> @@ -505,6 +514,7 @@ export function WebFetchTool(props: ToolProps) { } export function ReadTool(props: ToolProps) { + const messages = useShareMessages() const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd)) return ( @@ -518,10 +528,10 @@ export function ReadTool(props: ToolProps) { <div data-component="tool-result"> <Switch> <Match when={props.state.metadata?.error}> - <ContentError>{formatErrorString(props.state.output)}</ContentError> + <ContentError>{formatErrorString(props.state.output, messages.error)}</ContentError> </Match> <Match when={typeof props.state.metadata?.preview === "string"}> - <ResultsButton showCopy="Show preview" hideCopy="Hide preview"> + <ResultsButton showCopy={messages.show_preview} hideCopy={messages.hide_preview}> <ContentCode lang={getShikiLang(filePath() || "")} code={props.state.metadata?.preview} /> </ResultsButton> </Match> @@ -537,8 +547,11 @@ export function ReadTool(props: ToolProps) { } export function WriteTool(props: ToolProps) { + const messages = useShareMessages() const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd)) - const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath)) + const diagnostics = createMemo(() => + getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath, messages.error), + ) return ( <> @@ -554,10 +567,10 @@ export function WriteTool(props: ToolProps) { <div data-component="tool-result"> <Switch> <Match when={props.state.metadata?.error}> - <ContentError>{formatErrorString(props.state.output)}</ContentError> + <ContentError>{formatErrorString(props.state.output, messages.error)}</ContentError> </Match> <Match when={props.state.input?.content}> - <ResultsButton showCopy="Show contents" hideCopy="Hide contents"> + <ResultsButton showCopy={messages.show_contents} hideCopy={messages.hide_contents}> <ContentCode lang={getShikiLang(filePath() || "")} code={props.state.input?.content} /> </ResultsButton> </Match> @@ -568,8 +581,11 @@ export function WriteTool(props: ToolProps) { } export function EditTool(props: ToolProps) { + const messages = useShareMessages() const filePath = createMemo(() => stripWorkingDirectory(props.state.input.filePath, props.message.path.cwd)) - const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath)) + const diagnostics = createMemo(() => + getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath, messages.error), + ) return ( <> @@ -582,7 +598,7 @@ export function EditTool(props: ToolProps) { <div data-component="tool-result"> <Switch> <Match when={props.state.metadata?.error}> - <ContentError>{formatErrorString(props.state.metadata?.message || "")}</ContentError> + <ContentError>{formatErrorString(props.state.metadata?.message || "", messages.error)}</ContentError> </Match> <Match when={props.state.metadata?.diff}> <div data-component="diff"> @@ -609,6 +625,8 @@ export function BashTool(props: ToolProps) { } export function GlobTool(props: ToolProps) { + const messages = useShareMessages() + return ( <> <div data-component="tool-title"> @@ -619,7 +637,12 @@ export function GlobTool(props: ToolProps) { <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`} + showCopy={formatCount( + props.state.metadata?.count || 0, + messages.locale, + messages.result_one, + messages.result_other, + )} > <ContentText expand compact text={props.state.output} /> </ResultsButton> @@ -639,11 +662,12 @@ interface ResultsButtonProps extends ParentProps { } function ResultsButton(props: ResultsButtonProps) { const [show, setShow] = createSignal(false) + const messages = useShareMessages() 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>{show() ? props.hideCopy || messages.hide_results : props.showCopy || messages.show_results}</span> <span data-slot="icon"> <Show when={show()} fallback={<IconChevronRight width={11} height={11} />}> <IconChevronDown width={11} height={11} /> @@ -668,10 +692,19 @@ function Footer(props: ParentProps<{ title: string }>) { } function ToolFooter(props: { time: number }) { - return props.time > MIN_DURATION && <Footer title={`${props.time}ms`}>{formatDuration(props.time)}</Footer> + const messages = useShareMessages() + return ( + props.time > MIN_DURATION && ( + <Footer title={`${formatNumber(props.time, messages.locale)}ms`}> + {formatDuration(props.time, messages.locale)} + </Footer> + ) + ) } function TaskTool(props: ToolProps) { + const messages = useShareMessages() + return ( <> <div data-component="tool-title"> @@ -679,7 +712,7 @@ function TaskTool(props: ToolProps) { <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"> + <ResultsButton showCopy={messages.show_output} hideCopy={messages.hide_output}> <div data-component="tool-output"> <ContentMarkdown expand text={props.state.output} /> </div> @@ -700,7 +733,7 @@ export function FallbackTool(props: ToolProps) { <> <div></div> <div>{arg[0]}</div> - <div>{arg[1]}</div> + <div>{String(arg[1] ?? "")}</div> </> )} </For> @@ -720,10 +753,11 @@ export function FallbackTool(props: ToolProps) { // 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]> = [] +function flattenToolArgs(obj: unknown, prefix: string = ""): Array<[string, unknown]> { + const entries: Array<[string, unknown]> = [] + if (typeof obj !== "object" || obj === null) return entries - for (const [key, value] of Object.entries(obj)) { + for (const [key, value] of Object.entries(obj as Record<string, unknown>)) { const path = prefix ? `${prefix}.${key}` : key if (value !== null && typeof value === "object") { |
