mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(ui): localize chat surfaces
This commit is contained in:
@@ -50,6 +50,7 @@ describe("slash command browser import", () => {
|
||||
'import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";',
|
||||
'import type { CommandEntry } from "../../../../packages/gateway-protocol/src/index.js";',
|
||||
'import { buildBuiltinChatCommands } from "../../../../src/auto-reply/commands-registry.shared.js";',
|
||||
'import { t } from "../../i18n/index.ts";',
|
||||
'import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";',
|
||||
]);
|
||||
expect(importDeclarations(sharedRegistry)).toEqual([
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import type { CommandEntry } from "../../../../packages/gateway-protocol/src/index.js";
|
||||
import { buildBuiltinChatCommands } from "../../../../src/auto-reply/commands-registry.shared.js";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
|
||||
|
||||
export type SlashCommandCategory = "session" | "model" | "agents" | "tools";
|
||||
@@ -15,6 +16,7 @@ export type SlashCommandDef = {
|
||||
name: string;
|
||||
aliases?: string[];
|
||||
description: string;
|
||||
descriptionKey?: string;
|
||||
args?: string;
|
||||
icon?: ChatIconName;
|
||||
category?: SlashCommandCategory;
|
||||
@@ -100,6 +102,7 @@ const UI_ONLY_COMMANDS: SlashCommandDef[] = [
|
||||
key: "clear",
|
||||
name: "clear",
|
||||
description: "Clear chat history",
|
||||
descriptionKey: "chat.commands.clearDescription",
|
||||
icon: "trash",
|
||||
category: "session",
|
||||
executeLocal: true,
|
||||
@@ -109,6 +112,7 @@ const UI_ONLY_COMMANDS: SlashCommandDef[] = [
|
||||
key: "redirect",
|
||||
name: "redirect",
|
||||
description: "Abort and restart with a new message",
|
||||
descriptionKey: "chat.commands.redirectDescription",
|
||||
args: "<message>",
|
||||
icon: "refresh",
|
||||
category: "agents",
|
||||
@@ -145,6 +149,10 @@ const CATEGORY_OVERRIDES: Partial<Record<string, SlashCommandCategory>> = {
|
||||
queue: "model",
|
||||
};
|
||||
|
||||
const COMMAND_DESCRIPTION_KEYS: Partial<Record<string, string>> = {
|
||||
steer: "chat.commands.steerDescription",
|
||||
};
|
||||
|
||||
const COMMAND_DESCRIPTION_OVERRIDES: Partial<Record<string, string>> = {
|
||||
steer: "Inject a message into the active run",
|
||||
};
|
||||
@@ -235,6 +243,7 @@ function toSlashCommand(
|
||||
name,
|
||||
aliases: getSlashAliases(command).filter((alias) => alias !== name),
|
||||
description: COMMAND_DESCRIPTION_OVERRIDES[command.key] ?? command.description,
|
||||
descriptionKey: COMMAND_DESCRIPTION_KEYS[command.key],
|
||||
args: COMMAND_ARGS_OVERRIDES[command.key] ?? formatArgs(command),
|
||||
icon: mapIcon(command),
|
||||
category: mapCategory(command),
|
||||
@@ -428,12 +437,13 @@ export function resetSlashCommandsForTest(): void {
|
||||
|
||||
const CATEGORY_ORDER: SlashCommandCategory[] = ["session", "model", "tools", "agents"];
|
||||
|
||||
export const CATEGORY_LABELS: Record<SlashCommandCategory, string> = {
|
||||
session: "Session",
|
||||
model: "Model",
|
||||
agents: "Agents",
|
||||
tools: "Tools",
|
||||
};
|
||||
export function getSlashCommandCategoryLabel(category: SlashCommandCategory): string {
|
||||
return t(`chat.commands.categories.${category}`);
|
||||
}
|
||||
|
||||
export function getSlashCommandDescription(command: SlashCommandDef): string {
|
||||
return command.descriptionKey ? t(command.descriptionKey) : command.description;
|
||||
}
|
||||
|
||||
const TIER_ORDER: Record<SlashCommandTier, number> = {
|
||||
essential: 0,
|
||||
@@ -452,7 +462,7 @@ export function getSlashCommandCompletions(
|
||||
(cmd) =>
|
||||
cmd.name.startsWith(lower) ||
|
||||
cmd.aliases?.some((alias) => normalizeLowercaseStringOrEmpty(alias).startsWith(lower)) ||
|
||||
normalizeLowercaseStringOrEmpty(cmd.description).includes(lower),
|
||||
normalizeLowercaseStringOrEmpty(getSlashCommandDescription(cmd)).includes(lower),
|
||||
)
|
||||
: SLASH_COMMANDS;
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// @vitest-environment node
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { SLASH_COMMANDS } from "../../lib/chat/commands.ts";
|
||||
import {
|
||||
SLASH_COMMANDS,
|
||||
getSlashCommandCategoryLabel,
|
||||
getSlashCommandDescription,
|
||||
type SlashCommandDef,
|
||||
} from "../../lib/chat/commands.ts";
|
||||
import { refreshSlashCommands, resetChatSlashCommandMetadataForTest } from "./chat-commands.ts";
|
||||
|
||||
afterEach(() => {
|
||||
@@ -26,6 +31,16 @@ function expectRecordFields(value: unknown, label: string, expected: Record<stri
|
||||
}
|
||||
|
||||
describe("refreshSlashCommands", () => {
|
||||
it("resolves localized UI command metadata", () => {
|
||||
const clear = SLASH_COMMANDS.find((entry) => entry.name === "clear");
|
||||
const redirect = SLASH_COMMANDS.find((entry) => entry.name === "redirect");
|
||||
expect(getSlashCommandDescription(clear as SlashCommandDef)).toBe("Clear chat history");
|
||||
expect(getSlashCommandDescription(redirect as SlashCommandDef)).toBe(
|
||||
"Abort and restart with a new message",
|
||||
);
|
||||
expect(getSlashCommandCategoryLabel("tools")).toBe("Tools");
|
||||
});
|
||||
|
||||
it("exposes /learn through the browser fallback registry", () => {
|
||||
expectRecordFields(requireCommandByName("learn"), "learn command", {
|
||||
description: "Draft a reusable skill from recent work or named sources.",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isToolResultContentType,
|
||||
resolveToolUseId,
|
||||
} from "../../../../src/chat/tool-content.js";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import type {
|
||||
ChatItem,
|
||||
MessageGroup,
|
||||
@@ -39,6 +40,8 @@ import { buildUserChatMessageContentBlocks } from "./user-message-content.ts";
|
||||
|
||||
export type BuildChatItemsProps = {
|
||||
sessionKey: string;
|
||||
/** Invalidates cached display copy when the active UI language changes. */
|
||||
locale?: string;
|
||||
messages: unknown[];
|
||||
toolMessages: unknown[];
|
||||
streamSegments: ChatStreamSegment[];
|
||||
@@ -1149,12 +1152,11 @@ export function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | Mes
|
||||
typeof marker.id === "string"
|
||||
? `divider:compaction:${marker.id}`
|
||||
: `divider:compaction:${normalized.timestamp}:${i}`,
|
||||
label: "Compacted history",
|
||||
description:
|
||||
"The compacted transcript is preserved as a checkpoint. Open session checkpoints to branch or restore from that compacted view.",
|
||||
label: t("chat.compaction.label"),
|
||||
description: t("chat.compaction.description"),
|
||||
action: {
|
||||
kind: "session-checkpoints",
|
||||
label: "Open checkpoints",
|
||||
label: t("chat.compaction.openCheckpoints"),
|
||||
},
|
||||
timestamp: normalized.timestamp ?? Date.now(),
|
||||
});
|
||||
@@ -1371,6 +1373,7 @@ export function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | Mes
|
||||
function sameChatItemsInput(previous: BuildChatItemsProps, next: BuildChatItemsProps): boolean {
|
||||
return (
|
||||
previous.sessionKey === next.sessionKey &&
|
||||
previous.locale === next.locale &&
|
||||
previous.messages === next.messages &&
|
||||
previous.toolMessages === next.toolMessages &&
|
||||
previous.streamSegments === next.streamSegments &&
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "../../components/markdown.ts";
|
||||
import { i18n, t } from "../../i18n/index.ts";
|
||||
import type { ChatAttachment, ChatQueueItem } from "../../lib/chat/chat-types.ts";
|
||||
import { SLASH_COMMANDS } from "../../lib/chat/commands.ts";
|
||||
import { createSessionCapability, type SessionCapability } from "../../lib/sessions/index.ts";
|
||||
import {
|
||||
createModelCatalog,
|
||||
@@ -3395,6 +3396,32 @@ describe("chat slash menu accessibility", () => {
|
||||
expect(announcementText).toBe(expectedAnnouncement);
|
||||
});
|
||||
|
||||
it("uses the localized command description in the live announcement", async () => {
|
||||
const clearCommand = SLASH_COMMANDS.find((command) => command.name === "clear");
|
||||
if (!clearCommand) {
|
||||
throw new Error("Expected the clear slash command");
|
||||
}
|
||||
const originalDescriptionKey = clearCommand.descriptionKey;
|
||||
clearCommand.descriptionKey = "common.health";
|
||||
await i18n.setLocale("zh-CN");
|
||||
try {
|
||||
let draft = "";
|
||||
const onDraftChange = vi.fn((next: string) => {
|
||||
draft = next;
|
||||
});
|
||||
let container = renderChatView({ draft, onDraftChange });
|
||||
|
||||
inputDraft(container, "/clear");
|
||||
container = renderChatView({ draft, onDraftChange });
|
||||
|
||||
const status = container.querySelector<HTMLElement>("#chat-single-slash-active-announcement");
|
||||
expect(status?.textContent?.trim()).toBe(`/clear ${t("common.health")}`);
|
||||
} finally {
|
||||
clearCommand.descriptionKey = originalDescriptionKey;
|
||||
await i18n.setLocale("en");
|
||||
}
|
||||
});
|
||||
|
||||
it("wires fixed argument suggestions with command-and-argument option ids", () => {
|
||||
let draft = "";
|
||||
const onDraftChange = vi.fn((next: string) => {
|
||||
|
||||
@@ -325,7 +325,7 @@ export function renderChat(props: ChatProps) {
|
||||
class="chat-scroll-to-bottom"
|
||||
type="button"
|
||||
@click=${() => props.onScrollToBottom?.({ smooth: true })}
|
||||
aria-label="Scroll to latest"
|
||||
aria-label=${t("chat.actions.scrollToLatest")}
|
||||
>
|
||||
${icons.arrowDown}
|
||||
</button>
|
||||
@@ -378,12 +378,12 @@ export function renderChat(props: ChatProps) {
|
||||
<span class="callout__content">${props.error}</span>
|
||||
${props.onDismissError
|
||||
? html`
|
||||
<openclaw-tooltip content="Dismiss error">
|
||||
<openclaw-tooltip .content=${t("chat.actions.dismissError")}>
|
||||
<button
|
||||
class="callout__dismiss"
|
||||
type="button"
|
||||
@click=${props.onDismissError}
|
||||
aria-label="Dismiss error"
|
||||
aria-label=${t("chat.actions.dismissError")}
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
@@ -395,12 +395,12 @@ export function renderChat(props: ChatProps) {
|
||||
: nothing}
|
||||
${props.focusMode && props.onToggleFocusMode
|
||||
? html`
|
||||
<openclaw-tooltip content="Exit focus mode">
|
||||
<openclaw-tooltip .content=${t("chat.actions.exitFocusMode")}>
|
||||
<button
|
||||
class="chat-focus-exit"
|
||||
type="button"
|
||||
@click=${props.onToggleFocusMode}
|
||||
aria-label="Exit focus mode"
|
||||
aria-label=${t("chat.actions.exitFocusMode")}
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
|
||||
@@ -11,10 +11,11 @@ import "../../../components/tooltip.ts";
|
||||
import { t } from "../../../i18n/index.ts";
|
||||
import type { ChatAttachment, ChatQueueItem } from "../../../lib/chat/chat-types.ts";
|
||||
import {
|
||||
CATEGORY_LABELS,
|
||||
SLASH_COMMANDS,
|
||||
getHiddenCommandCount,
|
||||
getSlashCommandCategoryLabel,
|
||||
getSlashCommandCompletions,
|
||||
getSlashCommandDescription,
|
||||
type SlashCommandCategory,
|
||||
type SlashCommandDef,
|
||||
} from "../../../lib/chat/commands.ts";
|
||||
@@ -442,7 +443,7 @@ function renderChatGoal(
|
||||
${showActions && actions.onGoalEdit && goal.status !== "complete"
|
||||
? renderChatGoalActionButton({
|
||||
className: "agent-chat__goal-edit",
|
||||
label: "Edit goal",
|
||||
label: t("chat.goals.edit"),
|
||||
icon: icons.penLine,
|
||||
onClick: () => actions.onGoalEdit?.(goal),
|
||||
})
|
||||
@@ -450,7 +451,7 @@ function renderChatGoal(
|
||||
${showActions && goal.status === "active"
|
||||
? renderChatGoalActionButton({
|
||||
className: "agent-chat__goal-pause",
|
||||
label: "Pause goal",
|
||||
label: t("chat.goals.pause"),
|
||||
icon: icons.pause,
|
||||
onClick: () => actions.onGoalCommand?.("/goal pause"),
|
||||
})
|
||||
@@ -458,7 +459,7 @@ function renderChatGoal(
|
||||
${showActions && canResume
|
||||
? renderChatGoalActionButton({
|
||||
className: "agent-chat__goal-resume",
|
||||
label: "Resume goal",
|
||||
label: t("chat.goals.resume"),
|
||||
icon: icons.play,
|
||||
onClick: () => actions.onGoalCommand?.("/goal resume"),
|
||||
})
|
||||
@@ -466,7 +467,7 @@ function renderChatGoal(
|
||||
${showActions
|
||||
? renderChatGoalActionButton({
|
||||
className: "agent-chat__goal-clear",
|
||||
label: "Clear goal",
|
||||
label: t("chat.goals.clear"),
|
||||
icon: icons.trash,
|
||||
onClick: () => actions.onGoalCommand?.("/goal clear"),
|
||||
})
|
||||
@@ -743,7 +744,7 @@ function getActiveSlashMenuOptionLabel(state: ChatComposerState): string {
|
||||
return "";
|
||||
}
|
||||
const command = `/${cmd.name}${cmd.args ? ` ${cmd.args}` : ""}`;
|
||||
return `${command} ${cmd.description}`;
|
||||
return `${command} ${getSlashCommandDescription(cmd)}`;
|
||||
}
|
||||
|
||||
function scrollActiveSlashMenuOptionIntoView(state: ChatComposerState, paneId: string): void {
|
||||
@@ -801,10 +802,15 @@ function renderSlashMenu(
|
||||
state.slashMenuArgItems.length > 0
|
||||
) {
|
||||
return html`
|
||||
<div id=${listboxId} class="slash-menu" role="listbox" aria-label="Command arguments">
|
||||
<div
|
||||
id=${listboxId}
|
||||
class="slash-menu"
|
||||
role="listbox"
|
||||
aria-label=${t("chat.commands.arguments")}
|
||||
>
|
||||
<div class="slash-menu-group">
|
||||
<div class="slash-menu-group__label">
|
||||
/${state.slashMenuCommand.name} ${state.slashMenuCommand.description}
|
||||
/${state.slashMenuCommand.name} ${getSlashCommandDescription(state.slashMenuCommand)}
|
||||
</div>
|
||||
${state.slashMenuArgItems.map(
|
||||
(arg, i) => html`
|
||||
@@ -833,7 +839,9 @@ function renderSlashMenu(
|
||||
)}
|
||||
</div>
|
||||
<div class="slash-menu-footer">
|
||||
<kbd>↑↓</kbd> navigate <kbd>Tab</kbd> fill <kbd>Enter</kbd> run <kbd>Esc</kbd> close
|
||||
<kbd>↑↓</kbd> ${t("chat.commands.navigate")} <kbd>Tab</kbd> ${t("chat.commands.fill")}
|
||||
<kbd>Enter</kbd> ${t("chat.commands.run")} <kbd>Esc</kbd>
|
||||
${t("chat.commands.close")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -861,7 +869,7 @@ function renderSlashMenu(
|
||||
for (const [cat, entries] of grouped) {
|
||||
sections.push(html`
|
||||
<div class="slash-menu-group">
|
||||
<div class="slash-menu-group__label">${CATEGORY_LABELS[cat]}</div>
|
||||
<div class="slash-menu-group__label">${getSlashCommandCategoryLabel(cat)}</div>
|
||||
${entries.map(
|
||||
({ cmd, globalIdx }) => html`
|
||||
<div
|
||||
@@ -882,11 +890,15 @@ function renderSlashMenu(
|
||||
: nothing}
|
||||
<span class="slash-menu-name">/${cmd.name}</span>
|
||||
${cmd.args ? html`<span class="slash-menu-args">${cmd.args}</span>` : nothing}
|
||||
<span class="slash-menu-desc">${cmd.description}</span>
|
||||
<span class="slash-menu-desc">${getSlashCommandDescription(cmd)}</span>
|
||||
${cmd.argOptions?.length
|
||||
? html`<span class="slash-menu-badge">${cmd.argOptions.length} options</span>`
|
||||
? html`<span class="slash-menu-badge"
|
||||
>${t("chat.commands.optionCount", {
|
||||
count: String(cmd.argOptions.length),
|
||||
})}</span
|
||||
>`
|
||||
: cmd.executeLocal && !cmd.args
|
||||
? html` <span class="slash-menu-badge">instant</span> `
|
||||
? html` <span class="slash-menu-badge">${t("chat.commands.instant")}</span> `
|
||||
: nothing}
|
||||
</div>
|
||||
`,
|
||||
@@ -898,7 +910,7 @@ function renderSlashMenu(
|
||||
const hiddenCount = state.slashMenuExpanded ? 0 : getHiddenCommandCount();
|
||||
|
||||
return html`
|
||||
<div id=${listboxId} class="slash-menu" role="listbox" aria-label="Slash commands">
|
||||
<div id=${listboxId} class="slash-menu" role="listbox" aria-label=${t("chat.commands.menu")}>
|
||||
${sections}
|
||||
${hiddenCount > 0
|
||||
? html`<button
|
||||
@@ -910,11 +922,15 @@ function renderSlashMenu(
|
||||
updateSlashMenu(draft, requestUpdate, props);
|
||||
}}
|
||||
>
|
||||
Show ${hiddenCount} more command${hiddenCount !== 1 ? "s" : ""}
|
||||
${hiddenCount === 1
|
||||
? t("chat.commands.showMoreOne")
|
||||
: t("chat.commands.showMoreMany", { count: String(hiddenCount) })}
|
||||
</button>`
|
||||
: nothing}
|
||||
<div class="slash-menu-footer">
|
||||
<kbd>↑↓</kbd> navigate <kbd>Tab</kbd> fill <kbd>Enter</kbd> select <kbd>Esc</kbd> close
|
||||
<kbd>↑↓</kbd> ${t("chat.commands.navigate")} <kbd>Tab</kbd> ${t("chat.commands.fill")}
|
||||
<kbd>Enter</kbd> ${t("chat.commands.select")} <kbd>Esc</kbd>
|
||||
${t("chat.commands.close")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -989,7 +1005,9 @@ function renderChatQueueItem(item: ChatQueueItem, props: ChatQueueProps) {
|
||||
${failed ? icons.alertTriangle : icons.clock}
|
||||
</span>
|
||||
${steered
|
||||
? html`<span class="chat-queue__badge chat-queue__badge--steered">Steered</span>`
|
||||
? html`<span class="chat-queue__badge chat-queue__badge--steered"
|
||||
>${t("chat.queue.steered")}</span
|
||||
>`
|
||||
: nothing}
|
||||
${stateLabel ? html`<span class="chat-queue__badge">${stateLabel}</span>` : nothing}
|
||||
<span class="chat-queue__text" title=${text}>${text}</span>
|
||||
@@ -1012,22 +1030,22 @@ function renderChatQueueItem(item: ChatQueueItem, props: ChatQueueProps) {
|
||||
<button
|
||||
class="chat-queue__steer"
|
||||
type="button"
|
||||
aria-label="Steer queued message"
|
||||
aria-label=${t("chat.queue.steerQueuedMessage")}
|
||||
@click=${() => props.onQueueSteer?.(item.id)}
|
||||
>
|
||||
${icons.cornerDownRight}
|
||||
<span>Steer</span>
|
||||
<span>${t("chat.queue.steer")}</span>
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
${busy
|
||||
? nothing
|
||||
: html`
|
||||
<openclaw-tooltip content="Remove queued message">
|
||||
<openclaw-tooltip .content=${t("chat.queue.removeQueuedMessage")}>
|
||||
<button
|
||||
class="chat-queue__remove"
|
||||
type="button"
|
||||
aria-label="Remove queued message"
|
||||
aria-label=${t("chat.queue.removeQueuedMessage")}
|
||||
@click=${() => props.onQueueRemove(item.id)}
|
||||
>
|
||||
${icons.x}
|
||||
@@ -1251,11 +1269,11 @@ function renderAttachmentPreview(props: ChatAttachmentControlsProps) {
|
||||
</div>
|
||||
</openclaw-tooltip>
|
||||
`}
|
||||
<openclaw-tooltip content="Remove attachment">
|
||||
<openclaw-tooltip .content=${t("chat.composer.removeAttachment")}>
|
||||
<button
|
||||
class="chat-attachment-remove"
|
||||
type="button"
|
||||
aria-label="Remove attachment"
|
||||
aria-label=${t("chat.composer.removeAttachment")}
|
||||
@click=${() => {
|
||||
const next = (props.attachments ?? []).filter((a) => a.id !== att.id);
|
||||
releaseChatAttachmentPayload(att.id);
|
||||
@@ -1292,12 +1310,13 @@ export function renderChatRunStatusIndicator(status: ComposerRunStatus | null |
|
||||
if (elapsed >= CHAT_RUN_STATUS_TOAST_DURATION_MS) {
|
||||
return nothing;
|
||||
}
|
||||
const interrupted = t("chat.composer.runInterrupted");
|
||||
return html`
|
||||
<span
|
||||
class="agent-chat__run-status agent-chat__run-status--interrupted"
|
||||
aria-label="Run status: Interrupted"
|
||||
aria-label=${t("chat.composer.runStatus", { status: interrupted })}
|
||||
>
|
||||
${icons.stop}<span class="agent-chat__run-status-label">Interrupted</span>
|
||||
${icons.stop}<span class="agent-chat__run-status-label">${interrupted}</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
@@ -1857,7 +1876,7 @@ export function renderContextNotice(
|
||||
? "context-ring__action--busy"
|
||||
: ""}"
|
||||
type="button"
|
||||
aria-label="Compact recommended session context"
|
||||
aria-label=${t("chat.composer.compactRecommendedContext")}
|
||||
?disabled=${compactDisabled}
|
||||
@click=${(event: Event) => {
|
||||
event.preventDefault();
|
||||
@@ -2109,12 +2128,12 @@ export function renderChatComposer(props: ChatComposerProps) {
|
||||
const assistantName = props.assistantName || "OpenClaw";
|
||||
const inProgressLabel =
|
||||
submittedProgress?.sendState === "waiting-model"
|
||||
? "Preparing model..."
|
||||
? t("chat.composer.preparingModel")
|
||||
: props.stream !== null
|
||||
? `${assistantName} is responding...`
|
||||
? t("chat.composer.responding", { name: assistantName })
|
||||
: props.sending || submittedProgress
|
||||
? "Sending message..."
|
||||
: `${assistantName} is working...`;
|
||||
? t("chat.composer.sendingMessage")
|
||||
: t("chat.composer.working", { name: assistantName });
|
||||
// Persistent sr-only live region: run phases are otherwise conveyed only
|
||||
// visually (thread spark, content arriving, interrupted toast).
|
||||
const runStatusAnnouncement =
|
||||
@@ -2123,8 +2142,8 @@ export function renderChatComposer(props: ChatComposerProps) {
|
||||
: composerRunStatus.phase === "in-progress"
|
||||
? inProgressLabel
|
||||
: composerRunStatus.phase === "done"
|
||||
? "Done"
|
||||
: "Interrupted";
|
||||
? t("chat.composer.runDone")
|
||||
: t("chat.composer.runInterrupted");
|
||||
const requestUpdate = props.onRequestUpdate ?? (() => {});
|
||||
const sendShortcut = normalizeChatSendShortcut(props.sendShortcut);
|
||||
|
||||
@@ -2423,8 +2442,8 @@ export function renderChatComposer(props: ChatComposerProps) {
|
||||
type="button"
|
||||
class="chat-reply-preview__dismiss"
|
||||
@click=${() => props.onClearReply?.()}
|
||||
aria-label="Cancel reply"
|
||||
title="Cancel reply"
|
||||
aria-label=${t("chat.composer.cancelReply")}
|
||||
title=${t("chat.composer.cancelReply")}
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
|
||||
@@ -94,8 +94,8 @@ export function formatChatTimestampForDisplay(timestamp: number): ChatTimestampD
|
||||
const date = new Date(timestamp);
|
||||
if (!Number.isFinite(date.getTime())) {
|
||||
return {
|
||||
label: "Unknown date",
|
||||
title: "Unknown date",
|
||||
label: t("chat.messages.unknownDate"),
|
||||
title: t("chat.messages.unknownDate"),
|
||||
dateTime: "",
|
||||
};
|
||||
}
|
||||
@@ -133,7 +133,7 @@ const CHAT_RELATIVE_TIMESTAMP_FUTURE_SKEW_MS = 2 * 60 * 1000;
|
||||
export function formatChatRelativeTimestampLabel(timestamp: number, nowMs = Date.now()): string {
|
||||
const date = new Date(timestamp);
|
||||
if (!Number.isFinite(date.getTime())) {
|
||||
return "Unknown date";
|
||||
return t("chat.messages.unknownDate");
|
||||
}
|
||||
const ageMs = nowMs - date.getTime();
|
||||
// Derive from ageMs so the injected clock stays the single time source.
|
||||
@@ -815,7 +815,7 @@ export function renderMessageGroup(group: MessageGroup, opts: RenderMessageGroup
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="chat-group-footer">
|
||||
<span class="chat-sender-name">Activity</span>
|
||||
<span class="chat-sender-name">${t("chat.messages.activity")}</span>
|
||||
${renderChatTimestamp(group.timestamp)}
|
||||
${opts.onDelete ? renderDeleteButton(opts.onDelete, "right") : nothing}
|
||||
</div>
|
||||
@@ -1117,10 +1117,10 @@ function placeDeleteConfirmPopover(
|
||||
function renderDeleteButton(onDelete: () => void, side: DeleteConfirmSide) {
|
||||
return html`
|
||||
<span class="chat-delete-wrap">
|
||||
<openclaw-tooltip content="Delete">
|
||||
<openclaw-tooltip .content=${t("common.delete")}>
|
||||
<button
|
||||
class="chat-group-delete"
|
||||
aria-label="Delete message"
|
||||
aria-label=${t("chat.messages.deleteMessage")}
|
||||
@click=${(e: Event) => {
|
||||
if (shouldSkipDeleteConfirm()) {
|
||||
onDelete();
|
||||
@@ -1751,7 +1751,9 @@ function renderAssistantAttachments(
|
||||
>${availability.status === "checking" ? "Checking..." : "Unavailable"}</span
|
||||
>`
|
||||
: attachment.isVoiceNote
|
||||
? html`<span class="chat-assistant-attachment-badge">Voice note</span>`
|
||||
? html`<span class="chat-assistant-attachment-badge"
|
||||
>${t("chat.messages.voiceNote")}</span
|
||||
>`
|
||||
: nothing}
|
||||
</div>
|
||||
${attachmentUrl
|
||||
@@ -1917,11 +1919,11 @@ function renderExpandButton(
|
||||
},
|
||||
) {
|
||||
return html`
|
||||
<openclaw-tooltip content="Open in canvas">
|
||||
<openclaw-tooltip .content=${t("chat.messages.openInCanvas")}>
|
||||
<button
|
||||
class="btn btn--xs chat-expand-btn"
|
||||
type="button"
|
||||
aria-label="Open in canvas"
|
||||
aria-label=${t("chat.messages.openInCanvas")}
|
||||
@click=${() =>
|
||||
onOpenSidebar({
|
||||
kind: "markdown",
|
||||
@@ -2147,10 +2149,10 @@ function renderGroupedMessage(
|
||||
: `${toolNames.slice(0, 2).join(", ")} +${toolNames.length - 2} more`;
|
||||
const toolPreview = markdown ? (formatCollapsedToolPreviewText(markdown) ?? "") : "";
|
||||
const toolMessageLabelRaw = toolMessageHasError
|
||||
? "Tool error"
|
||||
? t("chat.toolCards.toolError")
|
||||
: singleToolDisplay && !markdown && !hasImages
|
||||
? singleToolDisplay.label
|
||||
: "Tool output";
|
||||
: t("chat.toolCards.toolOutput");
|
||||
const toolMessageLabel =
|
||||
formatCollapsedToolSummaryText(toolMessageLabelRaw) ?? toolMessageLabelRaw;
|
||||
const toolSummaryLabel = formatDistinctCollapsedToolSummaryText(
|
||||
|
||||
@@ -484,7 +484,9 @@ function renderChatModelReasoningSelect(params: {
|
||||
<span class="chat-controls__model-option-title">
|
||||
<span class="chat-controls__model-option-name">${modelLabel}</span>
|
||||
${entry.isDefault
|
||||
? html`<span class="chat-controls__model-default-label">Default</span>`
|
||||
? html`<span class="chat-controls__model-default-label"
|
||||
>${t("chat.modelControls.default")}</span
|
||||
>`
|
||||
: ""}
|
||||
</span>
|
||||
<span class="chat-controls__model-option-provider">
|
||||
@@ -605,7 +607,9 @@ function renderChatModelReasoningSelect(params: {
|
||||
${showReasoning
|
||||
? html`
|
||||
<div class="chat-controls__reasoning-head">
|
||||
<span class="chat-controls__inline-select-section-label">Reasoning</span>
|
||||
<span class="chat-controls__inline-select-section-label"
|
||||
>${t("chat.modelControls.reasoning")}</span
|
||||
>
|
||||
<span class="chat-controls__reasoning-state">
|
||||
<span
|
||||
class="chat-controls__reasoning-value ${hasThinkingOverride
|
||||
@@ -718,7 +722,9 @@ function renderChatModelReasoningSelect(params: {
|
||||
`
|
||||
: ""}
|
||||
<div class="chat-controls__speed-row">
|
||||
<span class="chat-controls__inline-select-section-label">Speed</span>
|
||||
<span class="chat-controls__inline-select-section-label"
|
||||
>${t("chat.modelControls.speed")}</span
|
||||
>
|
||||
<openclaw-tooltip .content=${speedTooltip}>
|
||||
<button
|
||||
class="chat-controls__speed-toggle ${fastMode.active
|
||||
|
||||
@@ -5,6 +5,7 @@ import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { icons } from "../../../components/icons.ts";
|
||||
import { toSanitizedMarkdownHtml } from "../../../components/markdown.ts";
|
||||
import "../../../components/tooltip.ts";
|
||||
import { t } from "../../../i18n/index.ts";
|
||||
import { buildSideChatFollowUpCommand } from "../../../lib/chat/side-question.ts";
|
||||
import type { ChatSideResult, ChatSideResultPending } from "../../../lib/chat/side-result.ts";
|
||||
import { detectTextDirection } from "../../../lib/text-direction.ts";
|
||||
@@ -50,7 +51,7 @@ function renderSideChatPendingTurn(pending: ChatSideResultPending): TemplateResu
|
||||
return html`
|
||||
<article class="chat-side-chat__turn chat-side-chat__turn--pending">
|
||||
<div class="chat-side-chat__question" dir=${detectTextDirection(question)}>${question}</div>
|
||||
<div class="chat-side-chat__thinking">Thinking…</div>
|
||||
<div class="chat-side-chat__thinking">${t("chat.sideChat.thinking")}</div>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
@@ -92,28 +93,28 @@ export function renderSideChatPanel(props: SideChatPanelProps): TemplateResult |
|
||||
input.value = "";
|
||||
};
|
||||
return html`
|
||||
<section class="chat-side-chat" role="dialog" aria-label="Side chat">
|
||||
<section class="chat-side-chat" role="dialog" aria-label=${t("chat.sideChat.title")}>
|
||||
<header class="chat-side-chat__header">
|
||||
<div class="chat-side-chat__heading">
|
||||
<h2 class="chat-side-chat__title">Side chat</h2>
|
||||
<span class="chat-side-chat__meta">Not saved to chat history</span>
|
||||
<h2 class="chat-side-chat__title">${t("chat.sideChat.title")}</h2>
|
||||
<span class="chat-side-chat__meta">${t("chat.sideChat.notSaved")}</span>
|
||||
</div>
|
||||
<div class="chat-side-chat__actions">
|
||||
<openclaw-tooltip content="Clear side chat">
|
||||
<openclaw-tooltip .content=${t("chat.sideChat.clear")}>
|
||||
<button
|
||||
class="btn btn--ghost btn--icon chat-icon-btn"
|
||||
type="button"
|
||||
aria-label="Clear side chat"
|
||||
aria-label=${t("chat.sideChat.clear")}
|
||||
@click=${() => props.onClear?.()}
|
||||
>
|
||||
${icons.trash}
|
||||
</button>
|
||||
</openclaw-tooltip>
|
||||
<openclaw-tooltip content="Close side chat">
|
||||
<openclaw-tooltip .content=${t("chat.sideChat.close")}>
|
||||
<button
|
||||
class="btn btn--ghost btn--icon chat-icon-btn"
|
||||
type="button"
|
||||
aria-label="Close side chat"
|
||||
aria-label=${t("chat.sideChat.close")}
|
||||
@click=${() => props.onClose?.()}
|
||||
>
|
||||
${icons.x}
|
||||
@@ -133,8 +134,8 @@ export function renderSideChatPanel(props: SideChatPanelProps): TemplateResult |
|
||||
<input
|
||||
class="chat-side-chat__input"
|
||||
type="text"
|
||||
placeholder=${pending ? "Thinking…" : "Follow up…"}
|
||||
aria-label="Follow up in side chat"
|
||||
placeholder=${pending ? t("chat.sideChat.thinking") : t("chat.sideChat.followUp")}
|
||||
aria-label=${t("chat.sideChat.followUpLabel")}
|
||||
.disabled=${pending != null}
|
||||
@keydown=${(event: KeyboardEvent) => {
|
||||
if (event.key !== "Enter" || event.isComposing) {
|
||||
@@ -147,7 +148,7 @@ export function renderSideChatPanel(props: SideChatPanelProps): TemplateResult |
|
||||
<button
|
||||
class="btn btn--ghost btn--icon chat-icon-btn chat-side-chat__send"
|
||||
type="button"
|
||||
aria-label="Send follow-up"
|
||||
aria-label=${t("chat.sideChat.sendFollowUp")}
|
||||
.disabled=${pending != null}
|
||||
@click=${(event: MouseEvent) => {
|
||||
const input = (event.currentTarget as HTMLElement)
|
||||
|
||||
@@ -286,11 +286,11 @@ function renderFileSidebarContent(
|
||||
<div class="sidebar-file-view__path-bar">
|
||||
<div class="sidebar-file-view__path-field">
|
||||
<span class="sidebar-file-view__path" title=${content.path}>${content.path}</span>
|
||||
<openclaw-tooltip content="Copy path">
|
||||
<openclaw-tooltip .content=${t("chat.detailPanel.copyPath")}>
|
||||
<button
|
||||
class="btn btn--sm sidebar-file-view__action"
|
||||
type="button"
|
||||
aria-label="Copy path"
|
||||
aria-label=${t("chat.detailPanel.copyPath")}
|
||||
@click=${() => void copyToClipboard(content.path)}
|
||||
>
|
||||
${icons.copy}
|
||||
@@ -316,17 +316,17 @@ function renderFileSidebarContent(
|
||||
?disabled=${controls.saving}
|
||||
@click=${controls.onDiscard}
|
||||
>
|
||||
Discard
|
||||
${t("chat.detailPanel.discard")}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
${content.edit
|
||||
? html`
|
||||
<openclaw-tooltip content="Edit file">
|
||||
<openclaw-tooltip .content=${t("chat.detailPanel.editFile")}>
|
||||
<button
|
||||
class="btn btn--sm sidebar-file-view__action"
|
||||
type="button"
|
||||
aria-label="Edit file"
|
||||
aria-label=${t("chat.detailPanel.editFile")}
|
||||
?disabled=${controls.loadingEditor}
|
||||
@click=${controls.onEdit}
|
||||
>
|
||||
@@ -335,11 +335,11 @@ function renderFileSidebarContent(
|
||||
</openclaw-tooltip>
|
||||
`
|
||||
: nothing}
|
||||
<openclaw-tooltip content="Search in file">
|
||||
<openclaw-tooltip .content=${t("chat.detailPanel.searchInFile")}>
|
||||
<button
|
||||
class="btn btn--sm sidebar-file-view__action"
|
||||
type="button"
|
||||
aria-label="Search in file"
|
||||
aria-label=${t("chat.detailPanel.searchInFile")}
|
||||
aria-pressed=${String(controls.searchOpen)}
|
||||
@click=${controls.onToggleSearch}
|
||||
>
|
||||
@@ -348,11 +348,11 @@ function renderFileSidebarContent(
|
||||
</openclaw-tooltip>
|
||||
${controls.onReveal
|
||||
? html`
|
||||
<openclaw-tooltip content="Show in Files">
|
||||
<openclaw-tooltip .content=${t("chat.detailPanel.showInFiles")}>
|
||||
<button
|
||||
class="btn btn--sm sidebar-file-view__action"
|
||||
type="button"
|
||||
aria-label="Show in Files"
|
||||
aria-label=${t("chat.detailPanel.showInFiles")}
|
||||
@click=${() => controls.onReveal?.(content.path)}
|
||||
>
|
||||
${icons.folder}
|
||||
@@ -417,8 +417,8 @@ function renderFileSidebarContent(
|
||||
<div class="file-view__search">
|
||||
<input
|
||||
type="search"
|
||||
aria-label="Search in file"
|
||||
placeholder="Search"
|
||||
aria-label=${t("chat.detailPanel.searchInFile")}
|
||||
placeholder=${t("common.search")}
|
||||
.value=${controls.query}
|
||||
@input=${(event: Event) =>
|
||||
controls.onSearchInput((event.currentTarget as HTMLInputElement).value)}
|
||||
@@ -430,7 +430,7 @@ function renderFileSidebarContent(
|
||||
<button
|
||||
class="btn btn--sm file-view__search-action file-view__search-action--previous"
|
||||
type="button"
|
||||
aria-label="Previous match"
|
||||
aria-label=${t("chat.detailPanel.previousMatch")}
|
||||
?disabled=${controls.matches.length === 0}
|
||||
@click=${controls.onPreviousMatch}
|
||||
>
|
||||
@@ -439,7 +439,7 @@ function renderFileSidebarContent(
|
||||
<button
|
||||
class="btn btn--sm file-view__search-action"
|
||||
type="button"
|
||||
aria-label="Next match"
|
||||
aria-label=${t("chat.detailPanel.nextMatch")}
|
||||
?disabled=${controls.matches.length === 0}
|
||||
@click=${controls.onNextMatch}
|
||||
>
|
||||
@@ -465,7 +465,7 @@ function renderFileSidebarContent(
|
||||
?disabled=${controls.saving}
|
||||
@click=${controls.onReload}
|
||||
>
|
||||
Reload
|
||||
${t("common.reload")}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
@@ -473,7 +473,7 @@ function renderFileSidebarContent(
|
||||
?disabled=${controls.saving}
|
||||
@click=${controls.onOverwrite}
|
||||
>
|
||||
Overwrite
|
||||
${t("chat.detailPanel.overwrite")}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
@@ -484,7 +484,7 @@ function renderFileSidebarContent(
|
||||
<div class="file-view">
|
||||
${keyed(controls?.mountKey ?? content, html`<div class="file-view__mount"></div>`)}
|
||||
${controls?.loadingEditor
|
||||
? html`<div class="file-view__loading muted">Loading…</div>`
|
||||
? html`<div class="file-view__loading muted">${t("common.loading")}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${controls?.editing
|
||||
@@ -492,7 +492,7 @@ function renderFileSidebarContent(
|
||||
: html`
|
||||
<div class="sidebar-file-view__footer">
|
||||
<button @click=${onViewRawText} class="btn btn--sm" type="button">
|
||||
View Raw Text
|
||||
${t("chat.detailPanel.viewRawText")}
|
||||
</button>
|
||||
</div>
|
||||
`}
|
||||
@@ -554,8 +554,13 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
||||
<div class="sidebar-panel">
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-title">${title}</div>
|
||||
<openclaw-tooltip content="Close sidebar">
|
||||
<button @click=${props.onClose} class="btn" type="button" aria-label="Close sidebar">
|
||||
<openclaw-tooltip .content=${t("chat.detailPanel.close")}>
|
||||
<button
|
||||
@click=${props.onClose}
|
||||
class="btn"
|
||||
type="button"
|
||||
aria-label=${t("chat.detailPanel.close")}
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
</openclaw-tooltip>
|
||||
@@ -572,7 +577,7 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
||||
type="button"
|
||||
style="margin-top: 12px;"
|
||||
>
|
||||
View Raw Text
|
||||
${t("chat.detailPanel.viewRawText")}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
@@ -605,7 +610,7 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
||||
? html`
|
||||
<div style="margin-top: 12px;">
|
||||
<button @click=${props.onViewRawText} class="btn" type="button">
|
||||
View Raw Text
|
||||
${t("chat.detailPanel.viewRawText")}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
@@ -627,7 +632,7 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
||||
? html`
|
||||
<div style="margin-top: 12px;">
|
||||
<button @click=${props.onViewRawText} class="btn" type="button">
|
||||
View Raw Text
|
||||
${t("chat.detailPanel.viewRawText")}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
@@ -640,14 +645,14 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
||||
<div class="sidebar-markdown-shell__intro">
|
||||
<div class="sidebar-markdown-shell__eyebrow">
|
||||
${icons.scrollText}
|
||||
<span>Rendered Markdown</span>
|
||||
<span>${t("chat.detailPanel.renderedMarkdown")}</span>
|
||||
</div>
|
||||
<div class="sidebar-markdown-shell__hint">
|
||||
Sanitized rich-text preview for quick reading.
|
||||
${t("chat.detailPanel.renderedMarkdownHint")}
|
||||
</div>
|
||||
</div>
|
||||
<button @click=${props.onViewRawText} class="btn btn--sm" type="button">
|
||||
View Raw Text
|
||||
${t("chat.detailPanel.viewRawText")}
|
||||
</button>
|
||||
</div>
|
||||
${markdownHtml
|
||||
@@ -658,12 +663,12 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
||||
`
|
||||
: html`
|
||||
<div class="sidebar-markdown-empty">
|
||||
No previewable markdown content.
|
||||
${t("chat.detailPanel.noPreviewableMarkdown")}
|
||||
</div>
|
||||
`}
|
||||
</section>
|
||||
`
|
||||
: html` <div class="muted">No content available</div> `}
|
||||
: html` <div class="muted">${t("chat.detailPanel.noContent")}</div> `}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
handleMarkdownCodeBlockCopy,
|
||||
markdownFileLinkFromEvent,
|
||||
} from "../../../components/markdown.ts";
|
||||
import { i18n, t } from "../../../i18n/index.ts";
|
||||
import { CHAT_HISTORY_RENDER_LIMIT } from "../../../lib/chat/chat-types.ts";
|
||||
import type { ChatQueueItem, ChatStreamSegment } from "../../../lib/chat/chat-types.ts";
|
||||
import { extractTextCached } from "../../../lib/chat/message-extract.ts";
|
||||
@@ -400,18 +401,18 @@ export function renderChatSearchBar(
|
||||
${icons.search}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search messages..."
|
||||
aria-label="Search messages"
|
||||
placeholder=${t("chat.thread.searchPlaceholder")}
|
||||
aria-label=${t("chat.thread.search")}
|
||||
.value=${state.searchQuery}
|
||||
@input=${(event: Event) => {
|
||||
state.searchQuery = (event.target as HTMLInputElement).value;
|
||||
requestUpdate();
|
||||
}}
|
||||
/>
|
||||
<openclaw-tooltip content="Close search">
|
||||
<openclaw-tooltip .content=${t("chat.thread.closeSearch")}>
|
||||
<button
|
||||
class="btn btn--ghost"
|
||||
aria-label="Close search"
|
||||
aria-label=${t("chat.thread.closeSearch")}
|
||||
@click=${() => {
|
||||
state.searchOpen = false;
|
||||
state.searchQuery = "";
|
||||
@@ -489,10 +490,10 @@ export function renderChatPinnedMessages(
|
||||
<span class="agent-chat__pinned-text"
|
||||
>${truncateUtf16Safe(text, 100)}${text.length > 100 ? "..." : ""}</span
|
||||
>
|
||||
<openclaw-tooltip content="Unpin">
|
||||
<openclaw-tooltip .content=${t("chat.thread.unpin")}>
|
||||
<button
|
||||
class="btn btn--ghost"
|
||||
aria-label="Unpin"
|
||||
aria-label=${t("chat.thread.unpin")}
|
||||
@click=${() => {
|
||||
pinned.unpin(index);
|
||||
requestUpdate();
|
||||
@@ -683,7 +684,7 @@ function handleChatContextMenu(event: MouseEvent, props: ChatThreadProps) {
|
||||
|
||||
function renderLoadingSkeleton() {
|
||||
return html`
|
||||
<div class="chat-loading-skeleton" aria-label="Loading chat">
|
||||
<div class="chat-loading-skeleton" aria-label=${t("chat.thread.loading")}>
|
||||
<div class="chat-line assistant">
|
||||
<div class="chat-msg">
|
||||
<div class="chat-bubble">
|
||||
@@ -749,8 +750,10 @@ export function renderChatThread(props: ChatThreadProps) {
|
||||
};
|
||||
const historyRenderLimit = resolveChatHistoryRenderWindow(props);
|
||||
const deleted = getDeletedMessages(props.sessionKey);
|
||||
const locale = i18n.getLocale();
|
||||
const chatItems = buildCachedChatItems({
|
||||
sessionKey: props.sessionKey,
|
||||
locale,
|
||||
messages: props.messages,
|
||||
toolMessages: props.toolMessages,
|
||||
streamSegments: props.streamSegments,
|
||||
@@ -828,11 +831,12 @@ export function renderChatThread(props: ChatThreadProps) {
|
||||
${showLoadingSkeleton ? renderLoadingSkeleton() : nothing}
|
||||
${isEmpty && !state.searchOpen ? renderWelcomeState(props) : nothing}
|
||||
${isEmpty && state.searchOpen
|
||||
? html` <div class="agent-chat__empty">No matching messages</div> `
|
||||
? html` <div class="agent-chat__empty">${t("chat.thread.noMatches")}</div> `
|
||||
: nothing}
|
||||
${guard(
|
||||
[
|
||||
chatItems,
|
||||
locale,
|
||||
deletedChatItemsSignature(deleted, chatItems),
|
||||
stableBooleanMapSignature(expandedToolCards),
|
||||
getAssistantAttachmentAvailabilityRenderVersion(),
|
||||
|
||||
@@ -29,6 +29,7 @@ vi.mock("../tool-display.ts", () => ({
|
||||
}));
|
||||
|
||||
import { resolveMcpAppSandboxUrl } from "../../../components/mcp-app-view.ts";
|
||||
import { t } from "../../../i18n/index.ts";
|
||||
import {
|
||||
formatDistinctCollapsedToolSummaryText,
|
||||
formatCollapsedToolPreviewText,
|
||||
@@ -774,6 +775,9 @@ describe("tool-cards", () => {
|
||||
const sidebarButton = container.querySelector<HTMLButtonElement>(".chat-tool-card__action-btn");
|
||||
expect(sidebarButton).toBeInstanceOf(HTMLButtonElement);
|
||||
expect([...sidebarButton!.classList]).toEqual(["chat-tool-card__action-btn"]);
|
||||
const tooltip = sidebarButton!.parentElement as HTMLElement & { content?: string };
|
||||
expect(tooltip.content).toBe(t("chat.toolCards.openDetails"));
|
||||
expect(sidebarButton!.getAttribute("aria-label")).toBe(t("chat.toolCards.openDetails"));
|
||||
sidebarButton!.click();
|
||||
|
||||
const sidebar = requireFirstMockArg(onOpenSidebar, "sidebar open");
|
||||
|
||||
@@ -256,7 +256,9 @@ export function renderToolPreview(
|
||||
return html`
|
||||
<div class="chat-tool-card__preview" data-kind="canvas" data-surface=${surface}>
|
||||
<div class="chat-tool-card__preview-header">
|
||||
<span class="chat-tool-card__preview-label">${preview.title?.trim() || "Canvas"}</span>
|
||||
<span class="chat-tool-card__preview-label"
|
||||
>${preview.title?.trim() || t("chat.toolCards.canvas")}</span
|
||||
>
|
||||
</div>
|
||||
<div class="chat-tool-card__preview-panel" data-side="canvas">
|
||||
${preview.mcpApp
|
||||
@@ -264,10 +266,10 @@ export function renderToolPreview(
|
||||
.sessionKey=${options?.sessionKey ?? ""}
|
||||
.viewId=${preview.mcpApp.viewId}
|
||||
.height=${preview.preferredHeight ?? 600}
|
||||
.title=${preview.title?.trim() || "MCP App"}
|
||||
.title=${preview.title?.trim() || t("mcpApp.title")}
|
||||
></mcp-app-view>`
|
||||
: renderPreviewFrame({
|
||||
title: preview.title?.trim() || "Canvas",
|
||||
title: preview.title?.trim() || t("chat.toolCards.canvas"),
|
||||
src: resolveCanvasIframeUrl(
|
||||
preview.url,
|
||||
options?.canvasPluginSurfaceUrl,
|
||||
@@ -643,7 +645,7 @@ function renderToolWorkspaceFilePath(
|
||||
<button
|
||||
class="chat-tool-card__detail chat-tool-card__detail-link"
|
||||
type="button"
|
||||
title="Open file"
|
||||
title=${t("chat.toolCards.openFile")}
|
||||
@click=${() => onOpenWorkspaceFile({ path })}
|
||||
>
|
||||
${label}
|
||||
@@ -845,12 +847,12 @@ export function renderExpandedToolCardContent(
|
||||
const sidebarAction = canOpenSidebar
|
||||
? html`
|
||||
<div class="chat-tool-card__actions">
|
||||
<openclaw-tooltip content="Open in the side panel">
|
||||
<openclaw-tooltip content=${t("chat.toolCards.openDetails")}>
|
||||
<button
|
||||
class="chat-tool-card__action-btn"
|
||||
type="button"
|
||||
@click=${() => onOpenSidebar?.(sidebarActionContent)}
|
||||
aria-label="Open tool details in side panel"
|
||||
aria-label=${t("chat.toolCards.openDetails")}
|
||||
>
|
||||
<span class="chat-tool-card__action-icon">${icons.panelRightOpen}</span>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user