summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/components/message-part.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/ui/src/components/message-part.tsx')
-rw-r--r--packages/ui/src/components/message-part.tsx140
1 files changed, 84 insertions, 56 deletions
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 4087705d1..8e2a36885 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -32,6 +32,7 @@ import { useData } from "../context"
import { useDiffComponent } from "../context/diff"
import { useCodeComponent } from "../context/code"
import { useDialog } from "../context/dialog"
+import { useI18n } from "../context/i18n"
import { BasicTool } from "./basic-tool"
import { GenericTool } from "./basic-tool"
import { Button } from "./button"
@@ -67,13 +68,14 @@ function getDiagnostics(
}
function DiagnosticsDisplay(props: { diagnostics: Diagnostic[] }): JSX.Element {
+ const i18n = useI18n()
return (
<Show when={props.diagnostics.length > 0}>
<div data-component="diagnostics">
<For each={props.diagnostics}>
{(diagnostic) => (
<div data-slot="diagnostic">
- <span data-slot="diagnostic-label">Error</span>
+ <span data-slot="diagnostic-label">{i18n.t("ui.messagePart.diagnostic.error")}</span>
<span data-slot="diagnostic-location">
[{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}]
</span>
@@ -179,81 +181,84 @@ export type ToolInfo = {
}
export function getToolInfo(tool: string, input: any = {}): ToolInfo {
+ const i18n = useI18n()
switch (tool) {
case "read":
return {
icon: "glasses",
- title: "Read",
+ title: i18n.t("ui.tool.read"),
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
}
case "list":
return {
icon: "bullet-list",
- title: "List",
+ title: i18n.t("ui.tool.list"),
subtitle: input.path ? getFilename(input.path) : undefined,
}
case "glob":
return {
icon: "magnifying-glass-menu",
- title: "Glob",
+ title: i18n.t("ui.tool.glob"),
subtitle: input.pattern,
}
case "grep":
return {
icon: "magnifying-glass-menu",
- title: "Grep",
+ title: i18n.t("ui.tool.grep"),
subtitle: input.pattern,
}
case "webfetch":
return {
icon: "window-cursor",
- title: "Webfetch",
+ title: i18n.t("ui.tool.webfetch"),
subtitle: input.url,
}
case "task":
return {
icon: "task",
- title: `${input.subagent_type || "task"} Agent`,
+ title: i18n.t("ui.tool.agent", { type: input.subagent_type || "task" }),
subtitle: input.description,
}
case "bash":
return {
icon: "console",
- title: "Shell",
+ title: i18n.t("ui.tool.shell"),
subtitle: input.description,
}
case "edit":
return {
icon: "code-lines",
- title: "Edit",
+ title: i18n.t("ui.messagePart.title.edit"),
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
}
case "write":
return {
icon: "code-lines",
- title: "Write",
+ title: i18n.t("ui.messagePart.title.write"),
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
}
case "apply_patch":
return {
icon: "code-lines",
- title: "Patch",
- subtitle: input.files?.length ? `${input.files.length} file${input.files.length > 1 ? "s" : ""}` : undefined,
+ title: i18n.t("ui.tool.patch"),
+ subtitle: input.files?.length
+ ? `${input.files.length} ${i18n.t(input.files.length > 1 ? "ui.common.file.other" : "ui.common.file.one")}`
+ : undefined,
}
case "todowrite":
return {
icon: "checklist",
- title: "To-dos",
+ title: i18n.t("ui.tool.todos"),
}
case "todoread":
return {
icon: "checklist",
- title: "Read to-dos",
+ title: i18n.t("ui.tool.todos.read"),
}
case "question":
return {
icon: "bubble-5",
- title: "Questions",
+ title: i18n.t("ui.tool.questions"),
}
default:
return {
@@ -297,6 +302,7 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
const dialog = useDialog()
+ const i18n = useI18n()
const [copied, setCopied] = createSignal(false)
const [expanded, setExpanded] = createSignal(false)
const [canExpand, setCanExpand] = createSignal(false)
@@ -385,7 +391,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
</div>
}
>
- <img data-slot="user-message-attachment-image" src={file.url} alt={file.filename ?? "attachment"} />
+ <img
+ data-slot="user-message-attachment-image"
+ src={file.url}
+ alt={file.filename ?? i18n.t("ui.message.attachment.alt")}
+ />
</Show>
</div>
)}
@@ -398,7 +408,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
<button
data-slot="user-message-expand"
type="button"
- aria-label={expanded() ? "Collapse message" : "Expand message"}
+ aria-label={expanded() ? i18n.t("ui.message.collapse") : i18n.t("ui.message.expand")}
onClick={(event) => {
event.stopPropagation()
toggleExpanded()
@@ -407,7 +417,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
<Icon name="chevron-down" size="small" />
</button>
<div data-slot="user-message-copy-wrapper">
- <Tooltip value={copied() ? "Copied!" : "Copy"} placement="top" gutter={8}>
+ <Tooltip value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top" gutter={8}>
<IconButton
icon={copied() ? "check" : "copy"}
variant="secondary"
@@ -529,6 +539,7 @@ export const ToolRegistry = {
PART_MAPPING["tool"] = function ToolPartDisplay(props) {
const data = useData()
+ const i18n = useI18n()
const part = props.part as ToolPart
const permission = createMemo(() => {
@@ -639,13 +650,13 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
<div data-component="permission-prompt">
<div data-slot="permission-actions">
<Button variant="ghost" size="small" onClick={() => respond("reject")}>
- Deny
+ {i18n.t("ui.permission.deny")}
</Button>
<Button variant="secondary" size="small" onClick={() => respond("always")}>
- Allow always
+ {i18n.t("ui.permission.allowAlways")}
</Button>
<Button variant="primary" size="small" onClick={() => respond("once")}>
- Allow once
+ {i18n.t("ui.permission.allowOnce")}
</Button>
</div>
</div>
@@ -687,6 +698,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
ToolRegistry.register({
name: "read",
render(props) {
+ const i18n = useI18n()
const args: string[] = []
if (props.input.offset) args.push("offset=" + props.input.offset)
if (props.input.limit) args.push("limit=" + props.input.limit)
@@ -695,7 +707,7 @@ ToolRegistry.register({
{...props}
icon="glasses"
trigger={{
- title: "Read",
+ title: i18n.t("ui.tool.read"),
subtitle: props.input.filePath ? getFilename(props.input.filePath) : "",
args,
}}
@@ -707,11 +719,12 @@ ToolRegistry.register({
ToolRegistry.register({
name: "list",
render(props) {
+ const i18n = useI18n()
return (
<BasicTool
{...props}
icon="bullet-list"
- trigger={{ title: "List", subtitle: getDirectory(props.input.path || "/") }}
+ trigger={{ title: i18n.t("ui.tool.list"), subtitle: getDirectory(props.input.path || "/") }}
>
<Show when={props.output}>
{(output) => (
@@ -728,12 +741,13 @@ ToolRegistry.register({
ToolRegistry.register({
name: "glob",
render(props) {
+ const i18n = useI18n()
return (
<BasicTool
{...props}
icon="magnifying-glass-menu"
trigger={{
- title: "Glob",
+ title: i18n.t("ui.tool.glob"),
subtitle: getDirectory(props.input.path || "/"),
args: props.input.pattern ? ["pattern=" + props.input.pattern] : [],
}}
@@ -753,6 +767,7 @@ ToolRegistry.register({
ToolRegistry.register({
name: "grep",
render(props) {
+ const i18n = useI18n()
const args: string[] = []
if (props.input.pattern) args.push("pattern=" + props.input.pattern)
if (props.input.include) args.push("include=" + props.input.include)
@@ -761,7 +776,7 @@ ToolRegistry.register({
{...props}
icon="magnifying-glass-menu"
trigger={{
- title: "Grep",
+ title: i18n.t("ui.tool.grep"),
subtitle: getDirectory(props.input.path || "/"),
args,
}}
@@ -781,12 +796,13 @@ ToolRegistry.register({
ToolRegistry.register({
name: "webfetch",
render(props) {
+ const i18n = useI18n()
return (
<BasicTool
{...props}
icon="window-cursor"
trigger={{
- title: "Webfetch",
+ title: i18n.t("ui.tool.webfetch"),
subtitle: props.input.url || "",
args: props.input.format ? ["format=" + props.input.format] : [],
action: (
@@ -812,6 +828,7 @@ ToolRegistry.register({
name: "task",
render(props) {
const data = useData()
+ const i18n = useI18n()
const summary = () =>
(props.metadata.summary ?? []) as { id: string; tool: string; state: { status: string; title?: string } }[]
@@ -899,7 +916,7 @@ ToolRegistry.register({
icon="task"
defaultOpen={true}
trigger={{
- title: `${props.input.subagent_type || props.tool} Agent`,
+ title: i18n.t("ui.tool.agent", { type: props.input.subagent_type || props.tool }),
titleClass: "capitalize",
subtitle: props.input.description,
}}
@@ -912,13 +929,13 @@ ToolRegistry.register({
<div data-component="permission-prompt">
<div data-slot="permission-actions">
<Button variant="ghost" size="small" onClick={() => respond("reject")}>
- Deny
+ {i18n.t("ui.permission.deny")}
</Button>
<Button variant="secondary" size="small" onClick={() => respond("always")}>
- Allow always
+ {i18n.t("ui.permission.allowAlways")}
</Button>
<Button variant="primary" size="small" onClick={() => respond("once")}>
- Allow once
+ {i18n.t("ui.permission.allowOnce")}
</Button>
</div>
</div>
@@ -929,7 +946,7 @@ ToolRegistry.register({
icon="task"
defaultOpen={true}
trigger={{
- title: `${props.input.subagent_type || props.tool} Agent`,
+ title: i18n.t("ui.tool.agent", { type: props.input.subagent_type || props.tool }),
titleClass: "capitalize",
subtitle: props.input.description,
}}
@@ -969,12 +986,13 @@ ToolRegistry.register({
ToolRegistry.register({
name: "bash",
render(props) {
+ const i18n = useI18n()
return (
<BasicTool
{...props}
icon="console"
trigger={{
- title: "Shell",
+ title: i18n.t("ui.tool.shell"),
subtitle: props.input.description,
}}
>
@@ -991,6 +1009,7 @@ ToolRegistry.register({
ToolRegistry.register({
name: "edit",
render(props) {
+ const i18n = useI18n()
const diffComponent = useDiffComponent()
const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath))
const filename = () => getFilename(props.input.filePath ?? "")
@@ -1001,7 +1020,9 @@ ToolRegistry.register({
trigger={
<div data-component="edit-trigger">
<div data-slot="message-part-title-area">
- <div data-slot="message-part-title">Edit {filename()}</div>
+ <div data-slot="message-part-title">
+ {i18n.t("ui.messagePart.title.edit")} {filename()}
+ </div>
<Show when={props.input.filePath?.includes("/")}>
<div data-slot="message-part-path">
<span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
@@ -1040,6 +1061,7 @@ ToolRegistry.register({
ToolRegistry.register({
name: "write",
render(props) {
+ const i18n = useI18n()
const codeComponent = useCodeComponent()
const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath))
const filename = () => getFilename(props.input.filePath ?? "")
@@ -1050,7 +1072,9 @@ ToolRegistry.register({
trigger={
<div data-component="write-trigger">
<div data-slot="message-part-title-area">
- <div data-slot="message-part-title">Write {filename()}</div>
+ <div data-slot="message-part-title">
+ {i18n.t("ui.messagePart.title.write")} {filename()}
+ </div>
<Show when={props.input.filePath?.includes("/")}>
<div data-slot="message-part-path">
<span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
@@ -1095,13 +1119,14 @@ interface ApplyPatchFile {
ToolRegistry.register({
name: "apply_patch",
render(props) {
+ const i18n = useI18n()
const diffComponent = useDiffComponent()
const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[])
const subtitle = createMemo(() => {
const count = files().length
if (count === 0) return ""
- return `${count} file${count > 1 ? "s" : ""}`
+ return `${count} ${i18n.t(count > 1 ? "ui.common.file.other" : "ui.common.file.one")}`
})
return (
@@ -1109,7 +1134,7 @@ ToolRegistry.register({
{...props}
icon="code-lines"
trigger={{
- title: "Patch",
+ title: i18n.t("ui.tool.patch"),
subtitle: subtitle(),
}}
>
@@ -1122,22 +1147,22 @@ ToolRegistry.register({
<Switch>
<Match when={file.type === "delete"}>
<span data-slot="apply-patch-file-action" data-type="delete">
- Deleted
+ {i18n.t("ui.patch.action.deleted")}
</span>
</Match>
<Match when={file.type === "add"}>
<span data-slot="apply-patch-file-action" data-type="add">
- Created
+ {i18n.t("ui.patch.action.created")}
</span>
</Match>
<Match when={file.type === "move"}>
<span data-slot="apply-patch-file-action" data-type="move">
- Moved
+ {i18n.t("ui.patch.action.moved")}
</span>
</Match>
<Match when={file.type === "update"}>
<span data-slot="apply-patch-file-action" data-type="update">
- Patched
+ {i18n.t("ui.patch.action.patched")}
</span>
</Match>
</Switch>
@@ -1171,6 +1196,7 @@ ToolRegistry.register({
ToolRegistry.register({
name: "todowrite",
render(props) {
+ const i18n = useI18n()
const todos = createMemo(() => {
const meta = props.metadata?.todos
if (Array.isArray(meta)) return meta
@@ -1193,7 +1219,7 @@ ToolRegistry.register({
defaultOpen
icon="checklist"
trigger={{
- title: "To-dos",
+ title: i18n.t("ui.tool.todos"),
subtitle: subtitle(),
}}
>
@@ -1218,6 +1244,7 @@ ToolRegistry.register({
ToolRegistry.register({
name: "question",
render(props) {
+ const i18n = useI18n()
const questions = createMemo(() => (props.input.questions ?? []) as QuestionInfo[])
const answers = createMemo(() => (props.metadata.answers ?? []) as QuestionAnswer[])
const completed = createMemo(() => answers().length > 0)
@@ -1225,8 +1252,8 @@ ToolRegistry.register({
const subtitle = createMemo(() => {
const count = questions().length
if (count === 0) return ""
- if (completed()) return `${count} answered`
- return `${count} question${count > 1 ? "s" : ""}`
+ if (completed()) return i18n.t("ui.question.subtitle.answered", { count })
+ return `${count} ${i18n.t(count > 1 ? "ui.common.question.other" : "ui.common.question.one")}`
})
return (
@@ -1235,7 +1262,7 @@ ToolRegistry.register({
defaultOpen={completed()}
icon="bubble-5"
trigger={{
- title: "Questions",
+ title: i18n.t("ui.tool.questions"),
subtitle: subtitle(),
}}
>
@@ -1247,7 +1274,7 @@ ToolRegistry.register({
return (
<div data-slot="question-answer-item">
<div data-slot="question-text">{q.question}</div>
- <div data-slot="answer-text">{answer().join(", ") || "(no answer)"}</div>
+ <div data-slot="answer-text">{answer().join(", ") || i18n.t("ui.question.answer.none")}</div>
</div>
)
}}
@@ -1261,6 +1288,7 @@ ToolRegistry.register({
function QuestionPrompt(props: { request: QuestionRequest }) {
const data = useData()
+ const i18n = useI18n()
const questions = createMemo(() => props.request.questions)
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
@@ -1387,7 +1415,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
}}
</For>
<button data-slot="question-tab" data-active={confirm()} onClick={() => selectTab(questions().length)}>
- Confirm
+ {i18n.t("ui.common.confirm")}
</button>
</div>
</Show>
@@ -1396,7 +1424,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
<div data-slot="question-content">
<div data-slot="question-text">
{question()?.question}
- {multi() ? " (select all that apply)" : ""}
+ {multi() ? " " + i18n.t("ui.question.multiHint") : ""}
</div>
<div data-slot="question-options">
<For each={options()}>
@@ -1420,7 +1448,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
data-picked={customPicked()}
onClick={() => selectOption(options().length)}
>
- <span data-slot="option-label">Type your own answer</span>
+ <span data-slot="option-label">{i18n.t("ui.messagePart.option.typeOwnAnswer")}</span>
<Show when={!store.editing && input()}>
<span data-slot="option-description">{input()}</span>
</Show>
@@ -1434,7 +1462,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
ref={(el) => setTimeout(() => el.focus(), 0)}
type="text"
data-slot="custom-input"
- placeholder="Type your answer..."
+ placeholder={i18n.t("ui.question.custom.placeholder")}
value={input()}
onInput={(e) => {
const inputs = [...store.custom]
@@ -1443,10 +1471,10 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
}}
/>
<Button type="submit" variant="primary" size="small">
- {multi() ? "Add" : "Submit"}
+ {multi() ? i18n.t("ui.common.add") : i18n.t("ui.common.submit")}
</Button>
<Button type="button" variant="ghost" size="small" onClick={() => setStore("editing", false)}>
- Cancel
+ {i18n.t("ui.common.cancel")}
</Button>
</form>
</Show>
@@ -1456,7 +1484,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
<Show when={confirm()}>
<div data-slot="question-review">
- <div data-slot="review-title">Review your answers</div>
+ <div data-slot="review-title">{i18n.t("ui.messagePart.review.title")}</div>
<For each={questions()}>
{(q, index) => {
const value = () => store.answers[index()]?.join(", ") ?? ""
@@ -1465,7 +1493,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
<div data-slot="review-item">
<span data-slot="review-label">{q.question}</span>
<span data-slot="review-value" data-answered={answered()}>
- {answered() ? value() : "(not answered)"}
+ {answered() ? value() : i18n.t("ui.question.review.notAnswered")}
</span>
</div>
)
@@ -1476,12 +1504,12 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
<div data-slot="question-actions">
<Button variant="ghost" size="small" onClick={reject}>
- Dismiss
+ {i18n.t("ui.common.dismiss")}
</Button>
<Show when={!single()}>
<Show when={confirm()}>
<Button variant="primary" size="small" onClick={submit}>
- Submit
+ {i18n.t("ui.common.submit")}
</Button>
</Show>
<Show when={!confirm() && multi()}>
@@ -1491,7 +1519,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
onClick={() => selectTab(store.tab + 1)}
disabled={(store.answers[store.tab]?.length ?? 0) === 0}
>
- Next
+ {i18n.t("ui.common.next")}
</Button>
</Show>
</Show>