refactor: remove aborted status banner from chat UI

Removes the transient aborted banner from the composer status area
Simplifies status row rendering to focus on working state and pending changes
Cleans up unused abort-status localization strings
This commit is contained in:
Bohdan Triapitsyn
2026-08-26 19:50:07 +03:00
parent 340738a33f
commit 717854d06b
15 changed files with 15 additions and 97 deletions
+8 -41
View File
@@ -419,7 +419,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
const ensureGitStatus = useGitStore((state) => state.ensureStatus); const ensureGitStatus = useGitStore((state) => state.ensureStatus);
const fetchGitStatus = useGitStore((state) => state.fetchStatus); const fetchGitStatus = useGitStore((state) => state.fetchStatus);
const clearGitDiffCache = useGitStore((state) => state.clearDiffCache); const clearGitDiffCache = useGitStore((state) => state.clearDiffCache);
const [showAbortStatus, setShowAbortStatus] = React.useState(false);
const setSessionAutoAccept = usePermissionStore((state) => state.setSessionAutoAccept); const setSessionAutoAccept = usePermissionStore((state) => state.setSessionAutoAccept);
const [isNarrowComposer, setIsNarrowComposer] = React.useState(false); const [isNarrowComposer, setIsNarrowComposer] = React.useState(false);
const [attachmentPreview, setAttachmentPreview] = React.useState<ToolPopupContent>({ const [attachmentPreview, setAttachmentPreview] = React.useState<ToolPopupContent>({
@@ -697,7 +696,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
attachments, attachments,
}; };
}, [resolveInlineFileMention]); }, [resolveInlineFileMention]);
const abortTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const prevWasAbortedRef = React.useRef(false); const prevWasAbortedRef = React.useRef(false);
// Issue linking state // Issue linking state
@@ -1721,29 +1719,15 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
containerRef: dropZoneRef, containerRef: dropZoneRef,
}); });
const startAbortIndicator = React.useCallback(() => {
if (abortTimeoutRef.current) {
clearTimeout(abortTimeoutRef.current);
abortTimeoutRef.current = null;
}
setShowAbortStatus(true);
abortTimeoutRef.current = setTimeout(() => {
setShowAbortStatus(false);
abortTimeoutRef.current = null;
}, 1800);
}, []);
const handleAbort = React.useCallback(() => { const handleAbort = React.useCallback(() => {
clearAbortPrompt(); clearAbortPrompt();
startAbortIndicator();
// btw mode: the stop button stops the fork's turn, not the main // btw mode: the stop button stops the fork's turn, not the main
// session's. // session's.
const abortTarget = isBtwActive && btwSessionId ? btwSessionId : currentSessionId; const abortTarget = isBtwActive && btwSessionId ? btwSessionId : currentSessionId;
void abortCurrentOperation(abortTarget || undefined); void abortCurrentOperation(abortTarget || undefined);
}, [abortCurrentOperation, btwSessionId, clearAbortPrompt, currentSessionId, isBtwActive, startAbortIndicator]); }, [abortCurrentOperation, btwSessionId, clearAbortPrompt, currentSessionId, isBtwActive]);
const handleCycleAgent = React.useCallback((direction: 1 | -1 = 1) => { const handleCycleAgent = React.useCallback((direction: 1 | -1 = 1) => {
const nextAgentName = getCycledPrimaryAgentName(agents, currentAgentName, direction); const nextAgentName = getCycledPrimaryAgentName(agents, currentAgentName, direction);
@@ -2592,31 +2576,15 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
handlePermissionAutoAcceptToggle(); handlePermissionAutoAcceptToggle();
}); });
// Acknowledging the abort record is what lets the working chip resume for
// the next run; the old "Aborted" banner that used to accompany it is gone.
React.useEffect(() => { React.useEffect(() => {
const pendingAbortBanner = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId; const pendingAbort = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId;
if (!prevWasAbortedRef.current && pendingAbortBanner && !showAbortStatus) { if (!prevWasAbortedRef.current && pendingAbort && currentSessionId) {
startAbortIndicator(); acknowledgeSessionAbort(currentSessionId);
if (currentSessionId) {
acknowledgeSessionAbort(currentSessionId);
}
} }
prevWasAbortedRef.current = pendingAbortBanner; prevWasAbortedRef.current = pendingAbort;
}, [ }, [abortPromptSessionId, acknowledgeSessionAbort, currentSessionId]);
abortPromptSessionId,
acknowledgeSessionAbort,
currentSessionId,
showAbortStatus,
startAbortIndicator,
]);
React.useEffect(() => {
return () => {
if (abortTimeoutRef.current) {
clearTimeout(abortTimeoutRef.current);
abortTimeoutRef.current = null;
}
};
}, []);
return ( return (
<> <>
@@ -2687,7 +2655,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
directory={currentSessionDirectoryForSync ?? currentDirectory} directory={currentSessionDirectoryForSync ?? currentDirectory}
/> />
<MemoComposerStatusBar <MemoComposerStatusBar
showAbortStatus={showAbortStatus}
showTodos={composerStatusExtrasEnabled} showTodos={composerStatusExtrasEnabled}
leftAccessory={!composerStatusExtrasEnabled || newSessionDraftOpen || !hasPendingChanges leftAccessory={!composerStatusExtrasEnabled || newSessionDraftOpen || !hasPendingChanges
? null ? null
@@ -116,13 +116,11 @@ const TodoItemRow: React.FC<{ todo: TodoItem }> = ({ todo }) => {
const EMPTY_TODOS: TodoItem[] = []; const EMPTY_TODOS: TodoItem[] = [];
interface ComposerStatusBarProps { interface ComposerStatusBarProps {
showAbortStatus?: boolean;
showTodos?: boolean; showTodos?: boolean;
leftAccessory?: React.ReactNode; leftAccessory?: React.ReactNode;
} }
export const ComposerStatusBar: React.FC<ComposerStatusBarProps> = ({ export const ComposerStatusBar: React.FC<ComposerStatusBarProps> = ({
showAbortStatus,
showTodos = true, showTodos = true,
leftAccessory, leftAccessory,
}) => { }) => {
@@ -186,7 +184,7 @@ export const ComposerStatusBar: React.FC<ComposerStatusBarProps> = ({
const hasTodoContent = showTodos && statusSummary.left > 0; const hasTodoContent = showTodos && statusSummary.left > 0;
const hasLeftAccessory = Boolean(leftAccessory); const hasLeftAccessory = Boolean(leftAccessory);
const hasContent = Boolean(showAbortStatus) || hasTodoContent || hasLeftAccessory; const hasContent = hasTodoContent || hasLeftAccessory;
const popoverRef = React.useRef<HTMLDivElement>(null); const popoverRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => { React.useEffect(() => {
@@ -252,16 +250,7 @@ export const ComposerStatusBar: React.FC<ComposerStatusBarProps> = ({
<div className={cn("flex items-center justify-between gap-2 h-8", hasLeftAccessory && "px-0.5")}> <div className={cn("flex items-center justify-between gap-2 h-8", hasLeftAccessory && "px-0.5")}>
{/* Left: abort status | pending-changes accessory */} {/* Left: abort status | pending-changes accessory */}
<div className={cn("flex-1 flex items-center min-w-0 gap-2", hasLeftAccessory ? "pl-1.5" : "overflow-x-hidden")}> <div className={cn("flex-1 flex items-center min-w-0 gap-2", hasLeftAccessory ? "pl-1.5" : "overflow-x-hidden")}>
{showAbortStatus ? ( {leftAccessory ?? null}
<div className="flex h-full items-center text-[var(--status-error)] pl-0.5">
<span className="flex items-center gap-1.5 typography-ui-label">
<Icon name="close-circle" aria-hidden="true" />
{t('chat.statusRow.aborted')}
</span>
</div>
) : leftAccessory ? (
leftAccessory
) : null}
</div> </div>
{/* Right: todos dropdown */} {/* Right: todos dropdown */}
+4 -18
View File
@@ -1,11 +1,9 @@
import React from "react"; import React from "react";
import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessionUIStore } from '@/sync/session-ui-store';
import { WorkingPlaceholder } from "./message/parts/WorkingPlaceholder"; import { WorkingPlaceholder } from "./message/parts/WorkingPlaceholder";
import { Icon } from "@/components/icon/Icon";
import { useI18n } from "@/lib/i18n";
// The floating assistant-status chip that hovers above the composer while the // The floating assistant-status chip that hovers above the composer while the
// agent works ("Claude is working…", abort notice). ONLY that. The composer's // agent works ("Claude is working…"). ONLY that. The composer's
// own bar — pending changes, todos dropdown — is ComposerStatusBar: they used // own bar — pending changes, todos dropdown — is ComposerStatusBar: they used
// to share this component, and every restyle of this chip (glass, placement) // to share this component, and every restyle of this chip (glass, placement)
// silently dragged the composer bar and its dropdown along with it. // silently dragged the composer bar and its dropdown along with it.
@@ -17,10 +15,8 @@ interface StatusRowProps {
statusText?: string | null; statusText?: string | null;
isGenericStatus?: boolean; isGenericStatus?: boolean;
isWaitingForPermission?: boolean; isWaitingForPermission?: boolean;
wasAborted?: boolean;
abortActive?: boolean; abortActive?: boolean;
retryInfo?: { attempt?: number; next?: number } | null; retryInfo?: { attempt?: number; next?: number } | null;
showAbortStatus?: boolean;
agentName?: string; agentName?: string;
modelName?: string | null; modelName?: string | null;
providerId?: string | null; providerId?: string | null;
@@ -31,19 +27,16 @@ export const StatusRow: React.FC<StatusRowProps> = ({
statusText = null, statusText = null,
isGenericStatus, isGenericStatus,
isWaitingForPermission, isWaitingForPermission,
wasAborted,
abortActive, abortActive,
retryInfo, retryInfo,
showAbortStatus,
agentName, agentName,
modelName, modelName,
providerId, providerId,
}) => { }) => {
const { t } = useI18n();
const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const shouldRenderPlaceholder = !showAbortStatus && (wasAborted || !abortActive); const shouldRenderPlaceholder = !abortActive;
const hasContent = isWorking || Boolean(wasAborted) || Boolean(showAbortStatus); const hasContent = isWorking;
if (!hasContent) { if (!hasContent) {
return null; return null;
@@ -63,14 +56,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
a shrink-to-fit wrapper around it always collapsed to zero. */} a shrink-to-fit wrapper around it always collapsed to zero. */}
<div className="oc-glass-popover inline-flex w-max max-w-full items-center gap-2 h-8 whitespace-nowrap rounded-full [corner-shape:round] px-3"> <div className="oc-glass-popover inline-flex w-max max-w-full items-center gap-2 h-8 whitespace-nowrap rounded-full [corner-shape:round] px-3">
<div className="flex items-center min-w-0 gap-2 overflow-x-hidden"> <div className="flex items-center min-w-0 gap-2 overflow-x-hidden">
{showAbortStatus ? ( {shouldRenderPlaceholder ? (
<div className="flex h-full items-center text-[var(--status-error)] pl-0.5">
<span className="flex items-center gap-1.5 typography-ui-label">
<Icon name="close-circle" aria-hidden="true"/>
{t('chat.statusRow.aborted')}
</span>
</div>
) : shouldRenderPlaceholder ? (
<WorkingPlaceholder <WorkingPlaceholder
key={currentSessionId ?? "no-session"} key={currentSessionId ?? "no-session"}
isWorking={isWorking} isWorking={isWorking}
@@ -2,7 +2,6 @@ import React from 'react';
import { useAssistantStatus } from '@/hooks/useAssistantStatus'; import { useAssistantStatus } from '@/hooks/useAssistantStatus';
import { useConfigStore } from '@/stores/useConfigStore'; import { useConfigStore } from '@/stores/useConfigStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { getProviderModelDisplayName } from '@/lib/modelDisplay'; import { getProviderModelDisplayName } from '@/lib/modelDisplay';
import { StatusRow } from './StatusRow'; import { StatusRow } from './StatusRow';
@@ -12,15 +11,6 @@ import { StatusRow } from './StatusRow';
* labels while still limiting subscriptions to the active assistant message. * labels while still limiting subscriptions to the active assistant message.
*/ */
export const StatusRowContainer: React.FC = React.memo(() => { export const StatusRowContainer: React.FC = React.memo(() => {
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const abortRecord = useSessionUIStore(
React.useCallback((state) => {
if (!currentSessionId) {
return null;
}
return state.sessionAbortFlags?.get(currentSessionId) ?? null;
}, [currentSessionId]),
);
const { activeModel, working } = useAssistantStatus(); const { activeModel, working } = useAssistantStatus();
const currentAgentName = useConfigStore((state) => state.currentAgentName); const currentAgentName = useConfigStore((state) => state.currentAgentName);
const providers = useConfigStore((state) => state.providers); const providers = useConfigStore((state) => state.providers);
@@ -35,16 +25,13 @@ export const StatusRowContainer: React.FC = React.memo(() => {
return getProviderModelDisplayName(provider, activeModel.modelId) || null; return getProviderModelDisplayName(provider, activeModel.modelId) || null;
}, [activeModel, providers]); }, [activeModel, providers]);
const wasAborted = Boolean(abortRecord && !abortRecord.acknowledged);
return ( return (
<StatusRow <StatusRow
isWorking={working.isWorking} isWorking={working.isWorking}
statusText={working.statusText} statusText={working.statusText}
isGenericStatus={working.isGenericStatus} isGenericStatus={working.isGenericStatus}
isWaitingForPermission={working.isWaitingForPermission} isWaitingForPermission={working.isWaitingForPermission}
wasAborted={wasAborted || working.wasAborted} abortActive={working.abortActive}
abortActive={wasAborted || working.abortActive}
retryInfo={working.retryInfo} retryInfo={working.retryInfo}
agentName={currentAgentName} agentName={currentAgentName}
modelName={modelDisplayName} modelName={modelDisplayName}
-1
View File
@@ -1903,7 +1903,6 @@ export const dict = {
'chat.statusRow.actions.stopGeneratingAria': 'Generierung stoppen', 'chat.statusRow.actions.stopGeneratingAria': 'Generierung stoppen',
'chat.statusRow.tasksTitle': 'Aufgaben', 'chat.statusRow.tasksTitle': 'Aufgaben',
'chat.statusRow.summary.activeLeft': '{active} aktiv · {left} übrig', 'chat.statusRow.summary.activeLeft': '{active} aktiv · {left} übrig',
'chat.statusRow.aborted': 'Abgebrochen',
'chat.revertIndicator.redo': 'Wiederholen', 'chat.revertIndicator.redo': 'Wiederholen',
'chat.revertIndicator.redoAria': 'Wiederholen — wiederhergestellte Nachrichten', 'chat.revertIndicator.redoAria': 'Wiederholen — wiederhergestellte Nachrichten',
'chat.revertPopover.title': 'Zurückgesetzt', 'chat.revertPopover.title': 'Zurückgesetzt',
-1
View File
@@ -2084,7 +2084,6 @@ export const dict = {
'chat.statusRow.tasksTitle': 'Tasks', 'chat.statusRow.tasksTitle': 'Tasks',
'chat.statusRow.modelStatus': '{model} is {status}', 'chat.statusRow.modelStatus': '{model} is {status}',
'chat.statusRow.summary.activeLeft': '{active} active · {left} left', 'chat.statusRow.summary.activeLeft': '{active} active · {left} left',
'chat.statusRow.aborted': 'Aborted',
'chat.revertIndicator.redo': 'Redo', 'chat.revertIndicator.redo': 'Redo',
'chat.revertIndicator.redoAria': 'Redo — restore reverted messages', 'chat.revertIndicator.redoAria': 'Redo — restore reverted messages',
'chat.revertPopover.title': 'Reverted', 'chat.revertPopover.title': 'Reverted',
-1
View File
@@ -2062,7 +2062,6 @@ export const dict: Record<I18nKey, string> = {
"chat.statusRow.tasksTitle": "Tareas", "chat.statusRow.tasksTitle": "Tareas",
"chat.statusRow.modelStatus": "{model} · {status}", "chat.statusRow.modelStatus": "{model} · {status}",
"chat.statusRow.summary.activeLeft": "{active} activas · {left} restantes", "chat.statusRow.summary.activeLeft": "{active} activas · {left} restantes",
"chat.statusRow.aborted": "Interrumpido",
"chat.revertIndicator.redo": "Rehacer", "chat.revertIndicator.redo": "Rehacer",
"chat.revertIndicator.redoAria": "Rehacer — restaurar mensajes revertidos", "chat.revertIndicator.redoAria": "Rehacer — restaurar mensajes revertidos",
"chat.revertPopover.title": "Revertidos", "chat.revertPopover.title": "Revertidos",
-1
View File
@@ -1826,7 +1826,6 @@ export const dict = {
'chat.statusRow.tasksTitle': 'Tâches', 'chat.statusRow.tasksTitle': 'Tâches',
'chat.statusRow.modelStatus': '{model} · {status}', 'chat.statusRow.modelStatus': '{model} · {status}',
'chat.statusRow.summary.activeLeft': '{active} actif · {left} gauche', 'chat.statusRow.summary.activeLeft': '{active} actif · {left} gauche',
'chat.statusRow.aborted': 'Avorté',
'chat.revertIndicator.redo': 'Refaire', 'chat.revertIndicator.redo': 'Refaire',
'chat.revertIndicator.redoAria': 'Rétablir : restaurer les messages annulés', 'chat.revertIndicator.redoAria': 'Rétablir : restaurer les messages annulés',
'chat.revertPopover.title': 'Rétabli', 'chat.revertPopover.title': 'Rétabli',
-1
View File
@@ -2080,7 +2080,6 @@ export const dict: Record<I18nKey, string> = {
'chat.statusRow.tasksTitle': 'タスク', 'chat.statusRow.tasksTitle': 'タスク',
'chat.statusRow.modelStatus': '{model} · {status}', 'chat.statusRow.modelStatus': '{model} · {status}',
'chat.statusRow.summary.activeLeft': '{active}アクティブ · {left}残り', 'chat.statusRow.summary.activeLeft': '{active}アクティブ · {left}残り',
'chat.statusRow.aborted': '中止されました',
'chat.revertIndicator.redo': 'やり直し', 'chat.revertIndicator.redo': 'やり直し',
'chat.revertIndicator.redoAria': 'やり直し — 元に戻したメッセージを復元', 'chat.revertIndicator.redoAria': 'やり直し — 元に戻したメッセージを復元',
'chat.revertPopover.title': '元に戻しました', 'chat.revertPopover.title': '元に戻しました',
-1
View File
@@ -2086,7 +2086,6 @@ export const dict: Record<I18nKey, string> = {
'chat.statusRow.tasksTitle': '작업', 'chat.statusRow.tasksTitle': '작업',
'chat.statusRow.modelStatus': '{model} · {status}', 'chat.statusRow.modelStatus': '{model} · {status}',
'chat.statusRow.summary.activeLeft': '{active}개 활성 · {left}개 남음', 'chat.statusRow.summary.activeLeft': '{active}개 활성 · {left}개 남음',
'chat.statusRow.aborted': '중단됨',
'chat.revertIndicator.redo': '다시 실행', 'chat.revertIndicator.redo': '다시 실행',
'chat.revertIndicator.redoAria': '다시 실행 — 되돌린 메시지 복원', 'chat.revertIndicator.redoAria': '다시 실행 — 되돌린 메시지 복원',
'chat.revertPopover.title': '되돌림', 'chat.revertPopover.title': '되돌림',
-1
View File
@@ -776,7 +776,6 @@ export const dict: Record<I18nKey, string> = {
'chat.statusRow.tasksTitle': 'Zadania', 'chat.statusRow.tasksTitle': 'Zadania',
'chat.statusRow.modelStatus': '{model} · {status}', 'chat.statusRow.modelStatus': '{model} · {status}',
'chat.statusRow.summary.activeLeft': '{active} aktywne · {left} pozostało', 'chat.statusRow.summary.activeLeft': '{active} aktywne · {left} pozostało',
'chat.statusRow.aborted': 'Przerwane',
'chat.revertIndicator.redo': 'Ponów', 'chat.revertIndicator.redo': 'Ponów',
'chat.revertIndicator.redoAria': 'Ponów — przywróć cofnięte wiadomości', 'chat.revertIndicator.redoAria': 'Ponów — przywróć cofnięte wiadomości',
'chat.revertPopover.title': 'Cofnięte', 'chat.revertPopover.title': 'Cofnięte',
@@ -2062,7 +2062,6 @@ export const dict: Record<I18nKey, string> = {
"chat.statusRow.tasksTitle": "Tarefas", "chat.statusRow.tasksTitle": "Tarefas",
"chat.statusRow.modelStatus": "{model} · {status}", "chat.statusRow.modelStatus": "{model} · {status}",
"chat.statusRow.summary.activeLeft": "{active} ativas · {left} restantes", "chat.statusRow.summary.activeLeft": "{active} ativas · {left} restantes",
"chat.statusRow.aborted": "Interrompido",
"chat.revertIndicator.redo": "Refazer", "chat.revertIndicator.redo": "Refazer",
"chat.revertIndicator.redoAria": "Refazer — restaurar mensagens revertidas", "chat.revertIndicator.redoAria": "Refazer — restaurar mensagens revertidas",
"chat.revertPopover.title": "Revertidas", "chat.revertPopover.title": "Revertidas",
-1
View File
@@ -2062,7 +2062,6 @@ export const dict: Record<I18nKey, string> = {
"chat.statusRow.tasksTitle": "завдання", "chat.statusRow.tasksTitle": "завдання",
"chat.statusRow.modelStatus": "{model} · {status}", "chat.statusRow.modelStatus": "{model} · {status}",
"chat.statusRow.summary.activeLeft": "Активних: {active} · залишилось: {left}", "chat.statusRow.summary.activeLeft": "Активних: {active} · залишилось: {left}",
"chat.statusRow.aborted": "Перервано",
"chat.revertIndicator.redo": "Повторити", "chat.revertIndicator.redo": "Повторити",
"chat.revertIndicator.redoAria": "Повторити — відновити відкочені повідомлення", "chat.revertIndicator.redoAria": "Повторити — відновити відкочені повідомлення",
"chat.revertPopover.title": "Відкочено", "chat.revertPopover.title": "Відкочено",
@@ -2050,7 +2050,6 @@ export const dict: Record<I18nKey, string> = {
'chat.statusRow.tasksTitle': '任务', 'chat.statusRow.tasksTitle': '任务',
'chat.statusRow.modelStatus': '{model} · {status}', 'chat.statusRow.modelStatus': '{model} · {status}',
'chat.statusRow.summary.activeLeft': '{active} 个活跃 · 剩余 {left} 个', 'chat.statusRow.summary.activeLeft': '{active} 个活跃 · 剩余 {left} 个',
'chat.statusRow.aborted': '已中止',
'chat.revertIndicator.redo': '重做', 'chat.revertIndicator.redo': '重做',
'chat.revertIndicator.redoAria': '重做 — 恢复已撤回的消息', 'chat.revertIndicator.redoAria': '重做 — 恢复已撤回的消息',
'chat.revertPopover.title': '已撤回', 'chat.revertPopover.title': '已撤回',
@@ -2054,7 +2054,6 @@ export const dict: Record<I18nKey, string> = {
'chat.statusRow.tasksTitle': '任務', 'chat.statusRow.tasksTitle': '任務',
'chat.statusRow.modelStatus': '{model} · {status}', 'chat.statusRow.modelStatus': '{model} · {status}',
'chat.statusRow.summary.activeLeft': '{active} 個活躍 · 剩餘 {left} 個', 'chat.statusRow.summary.activeLeft': '{active} 個活躍 · 剩餘 {left} 個',
'chat.statusRow.aborted': '已中止',
'chat.revertIndicator.redo': '重做', 'chat.revertIndicator.redo': '重做',
'chat.revertIndicator.redoAria': '重做 — 恢復已收回的訊息', 'chat.revertIndicator.redoAria': '重做 — 恢復已收回的訊息',
'chat.revertPopover.title': '已收回', 'chat.revertPopover.title': '已收回',