From 537c39f771e67ad4ca4c8e9203aced7a020c548d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ren=C3=A9=20Wang?=
<52880665+rivertwilight@users.noreply.github.com>
Date: Thu, 4 Jun 2026 21:24:28 +0800
Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20style(chat-input):=20rework=20Pl?=
=?UTF-8?q?us=20menu=20with=20toggle=20switches=20and=20grouped=20submenus?=
=?UTF-8?q?=20(#15433)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
locales/en-US/chat.json | 3 +-
locales/en-US/editor.json | 1 +
locales/zh-CN/chat.json | 3 +-
locales/zh-CN/editor.json | 1 +
.../ActionBar/Knowledge/useControls.tsx | 40 +++---
.../ChatInput/ActionBar/Plus/index.tsx | 114 +++++++++---------
.../ChatInput/ActionBar/Tools/useControls.tsx | 54 ++++++---
.../ChatInput/ActionBar/Upload/index.tsx | 17 ++-
.../ActionBar/components/ActionDropdown.tsx | 42 +++++--
src/locales/default/chat.ts | 1 +
src/locales/default/editor.ts | 1 +
11 files changed, 164 insertions(+), 113 deletions(-)
diff --git a/locales/en-US/chat.json b/locales/en-US/chat.json
index d668e2e5d5..e404778ec0 100644
--- a/locales/en-US/chat.json
+++ b/locales/en-US/chat.json
@@ -208,6 +208,7 @@
"heteroAgent.cloudRepo.noRepos": "No repositories configured. Add them in agent settings.",
"heteroAgent.cloudRepo.notSet": "No repo selected",
"heteroAgent.cloudRepo.sectionTitle": "Repositories",
+ "heteroAgent.executionTarget.downloadDesktop": "Get Desktop App",
"heteroAgent.executionTarget.infoTooltip": "Pick a remote device to drive that machine from the web. \"This device\" runs the agent locally and is only available inside the desktop app.",
"heteroAgent.executionTarget.loading": "Loading devices…",
"heteroAgent.executionTarget.local": "This device",
@@ -217,7 +218,6 @@
"heteroAgent.executionTarget.online": "Online",
"heteroAgent.executionTarget.sandbox": "Cloud sandbox",
"heteroAgent.executionTarget.sandboxDesc": "Run in an ephemeral cloud sandbox",
- "heteroAgent.executionTarget.downloadDesktop": "Get Desktop App",
"heteroAgent.executionTarget.title": "Execution Device",
"heteroAgent.executionTarget.unknownDevice": "Unknown device",
"heteroAgent.fullAccess.label": "Full access",
@@ -412,6 +412,7 @@
"platformAgent.deviceGuard.platformUnavailable.desc": "{{name}} is not installed on the connected device.",
"platformAgent.deviceGuard.platformUnavailable.title": "{{name}} not available",
"platformAgent.deviceGuard.refresh": "Refresh",
+ "plus.addAttachments": "Attachments",
"plus.addSkills": "Add Skills...",
"plus.search.appSearch": "Smart Search",
"plus.search.appSearchDesc": "LobeHub optimized search service, delivering best retrieval results.",
diff --git a/locales/en-US/editor.json b/locales/en-US/editor.json
index 789f9c032b..680ac327e1 100644
--- a/locales/en-US/editor.json
+++ b/locales/en-US/editor.json
@@ -13,6 +13,7 @@
"actions.expand.on": "Expand",
"actions.typobar.off": "Hide formatting toolbar",
"actions.typobar.on": "Show formatting toolbar",
+ "actions.typobar.title": "Formatting Tool",
"autoSave.latest": "Latest version loaded",
"autoSave.saved": "Saved",
"autoSave.saving": "Auto-saving...",
diff --git a/locales/zh-CN/chat.json b/locales/zh-CN/chat.json
index f7b6e4ea9e..36ad695ebc 100644
--- a/locales/zh-CN/chat.json
+++ b/locales/zh-CN/chat.json
@@ -208,6 +208,7 @@
"heteroAgent.cloudRepo.noRepos": "未配置仓库,请在助理设置中添加。",
"heteroAgent.cloudRepo.notSet": "未选择仓库",
"heteroAgent.cloudRepo.sectionTitle": "代码仓库",
+ "heteroAgent.executionTarget.downloadDesktop": "下载桌面端",
"heteroAgent.executionTarget.infoTooltip": "选择「远程设备」后可在网页中驱动该机器;「本机」仅在桌面端内运行 agent。",
"heteroAgent.executionTarget.loading": "正在加载设备…",
"heteroAgent.executionTarget.local": "本机",
@@ -217,7 +218,6 @@
"heteroAgent.executionTarget.online": "在线",
"heteroAgent.executionTarget.sandbox": "云端沙箱",
"heteroAgent.executionTarget.sandboxDesc": "在临时云端沙箱中运行",
- "heteroAgent.executionTarget.downloadDesktop": "下载桌面端",
"heteroAgent.executionTarget.title": "执行设备",
"heteroAgent.executionTarget.unknownDevice": "未知设备",
"heteroAgent.fullAccess.label": "完全访问权限",
@@ -412,6 +412,7 @@
"platformAgent.deviceGuard.platformUnavailable.desc": "连接的设备上未安装 {{name}}。",
"platformAgent.deviceGuard.platformUnavailable.title": "{{name}} 不可用",
"platformAgent.deviceGuard.refresh": "刷新",
+ "plus.addAttachments": "附件",
"plus.addSkills": "添加技能...",
"plus.search.appSearch": "智能检索",
"plus.search.appSearchDesc": "LobeHub 专属调优的搜索服务,检索效果最佳。",
diff --git a/locales/zh-CN/editor.json b/locales/zh-CN/editor.json
index 44c6ce4bd2..b9c4a226fa 100644
--- a/locales/zh-CN/editor.json
+++ b/locales/zh-CN/editor.json
@@ -13,6 +13,7 @@
"actions.expand.on": "展开",
"actions.typobar.off": "隐藏格式工具栏",
"actions.typobar.on": "显示格式工具栏",
+ "actions.typobar.title": "格式工具",
"autoSave.latest": "已加载最新版本",
"autoSave.saved": "已保存",
"autoSave.saving": "自动保存中…",
diff --git a/src/features/ChatInput/ActionBar/Knowledge/useControls.tsx b/src/features/ChatInput/ActionBar/Knowledge/useControls.tsx
index 432400348f..395e5bef7d 100644
--- a/src/features/ChatInput/ActionBar/Knowledge/useControls.tsx
+++ b/src/features/ChatInput/ActionBar/Knowledge/useControls.tsx
@@ -14,7 +14,10 @@ import { agentByIdSelectors } from '@/store/agent/selectors';
import { useAgentId } from '../../hooks/useAgentId';
import CheckboxItem from '../components/CheckboxWithLoading';
-const labelMaxWidth = 'min(400px, 56vw)';
+// Cap so the widest library/file row (icon + label + checkbox + paddings) stays within the
+// submenu's 320px footer-driven width, keeping it level with the skill submenu instead of
+// growing past it.
+const labelMaxWidth = 'min(210px, 45vw)';
const styles = createStaticStyles(({ css }) => ({
viewMore: css`
@@ -24,9 +27,17 @@ const styles = createStaticStyles(({ css }) => ({
gap: 8px;
align-items: center;
- width: calc(100% + 8px);
+ /* width 320 + margin-inline -12 anchors the submenu to 320px (matching the skill
+ submenu) and lets the row span full width; padding-inline 12 lines its icon/text
+ up with the menu items above. */
+ width: 320px;
min-height: 32px;
- margin-inline-start: -4px;
+
+ /* The footer wrapper adds padding-block: 8px top & bottom; the top keeps it separated
+ from the list, but the bottom leaves a dead gap against the popup edge — cancel it. */
+ margin-block-end: -8px;
+ margin-inline: -12px;
+ padding-inline: 12px;
border: 0;
border-radius: 6px;
@@ -107,28 +118,11 @@ export const useControls = ({
),
}));
+ // Flat list (no "Libraries" / "Files" group headers): libraries first, then files.
const relatedGroups: ItemType[] = [
- ...(libraryItems.length > 0
- ? [
- {
- children: libraryItems,
- key: 'relativeLibraries',
- label: t('knowledgeBase.libraries'),
- type: 'group' as const,
- },
- ]
- : []),
+ ...libraryItems,
...(libraryItems.length > 0 && fileItems.length > 0 ? [{ type: 'divider' as const }] : []),
- ...(fileItems.length > 0
- ? [
- {
- children: fileItems,
- key: 'relativeFiles',
- label: t('knowledgeBase.files'),
- type: 'group' as const,
- },
- ]
- : []),
+ ...fileItems,
];
const footer = (
diff --git a/src/features/ChatInput/ActionBar/Plus/index.tsx b/src/features/ChatInput/ActionBar/Plus/index.tsx
index a369dcfd97..cb00c365a1 100644
--- a/src/features/ChatInput/ActionBar/Plus/index.tsx
+++ b/src/features/ChatInput/ActionBar/Plus/index.tsx
@@ -3,7 +3,7 @@
import { validateVideoFileSize } from '@lobechat/utils/client';
import type { IconProps } from '@lobehub/ui';
import { Icon, Popover } from '@lobehub/ui';
-import { BrainOffIcon, GlobeOffIcon, SkillsIcon } from '@lobehub/ui/icons';
+import { GlobeOffIcon, SkillsIcon } from '@lobehub/ui/icons';
import { Upload } from 'antd';
import { css, cssVar, cx } from 'antd-style';
import {
@@ -16,7 +16,6 @@ import {
PlusIcon,
SearchCheck,
Settings2Icon,
- Store,
TypeIcon,
} from 'lucide-react';
import type { ReactNode } from 'react';
@@ -25,7 +24,6 @@ import { useTranslation } from 'react-i18next';
import { message } from '@/components/AntdStaticMethods';
import { openAttachKnowledgeModal } from '@/features/LibraryModal';
-import { createSkillStoreModal } from '@/features/SkillStore';
import { useModelSupportToolUse } from '@/hooks/useModelSupportToolUse';
import { useVisualMediaUploadAbility } from '@/hooks/useVisualMediaUploadAbility';
import { useAgentStore } from '@/store/agent';
@@ -235,7 +233,10 @@ const stripPopoverContent = (items?: ActionDropdownMenuItems): ActionDropdownMen
delete nextItem.popoverContent;
if ('children' in nextItem && nextItem.children) {
- return { ...nextItem, children: stripPopoverContent(nextItem.children) };
+ return {
+ ...nextItem,
+ children: stripPopoverContent(nextItem.children),
+ } as ActionDropdownMenuItems[number];
}
if ('label' in nextItem) {
@@ -315,9 +316,12 @@ const PlusAction = memo(() => {
const activeSearchOption: 'off' | 'app' | 'provider' =
searchMode === 'off' ? 'off' : useModelBuiltinSearch ? 'provider' : 'app';
- const handleToggleMemory = useCallback(async () => {
- await updateAgentChatConfig({ memory: { enabled: !isMemoryEnabled } });
- }, [isMemoryEnabled, updateAgentChatConfig]);
+ const handleToggleMemory = useCallback(
+ async (enabled: boolean) => {
+ await updateAgentChatConfig({ memory: { enabled } });
+ },
+ [updateAgentChatConfig],
+ );
const handleSelectSearch = useCallback(
async (option: 'off' | 'app' | 'provider') => {
@@ -332,11 +336,6 @@ const PlusAction = memo(() => {
[updateAgentChatConfig],
);
- const handleOpenTools = useCallback(() => {
- setDropdownOpen(false);
- createSkillStoreModal();
- }, []);
-
const handleToggleParams = useCallback(() => {
setDropdownOpen(false);
if (isParamsPanelActive) {
@@ -399,7 +398,8 @@ const PlusAction = memo(() => {
const uploadItems: ActionDropdownMenuItems = [
{
closeOnClick: false,
- icon: FileUp,
+ // Match the 20px file/library icons below so the label lines up with those rows.
+ icon: ,
key: 'upload-file-or-image',
label: (
{
const toolsItems: ActionDropdownMenuItems =
isAgentModeEnabled && enableFC
? [
- { type: 'divider' },
{
children: skillMenuItems,
footer: skillMarketFooter,
header: skillMarketHeader,
- icon: activeIcon(SkillsIcon, activeSkillCount > 0),
+ icon: SkillsIcon,
key: 'tools',
label: renderLabelWithCount(
tSetting('tools.title'),
@@ -452,38 +451,19 @@ const PlusAction = memo(() => {
),
),
} as ActionDropdownMenuItems[number],
- {
- icon: Store,
- key: 'add-skills',
- label: t('plus.addSkills'),
- onClick: handleOpenTools,
- },
+ { type: 'divider' },
]
: [];
const capabilityItems: ActionDropdownMenuItems = [
- { type: 'divider' },
- // Rich text toolbar toggle
+ // Memory toggle — trailing switch; toggle by clicking the switch or the whole row
{
- icon: TypeIcon,
- key: 'typo',
- label: renderActive(tEditor('actions.typobar.on'), Boolean(showTypoBar)),
- onClick: () => setShowTypoBar(!showTypoBar),
- },
- // Advanced parameter settings — mirrors ParamsPanelToggle in the agent header.
- {
- icon: Settings2Icon,
- key: 'params',
- label: renderActive(tSetting('settingModel.params.title'), isParamsPanelActive),
- onClick: handleToggleParams,
- },
- { type: 'divider' },
- // Memory toggle
- {
- icon: activeIcon(isMemoryEnabled ? Brain : BrainOffIcon, Boolean(isMemoryEnabled)),
+ checked: Boolean(isMemoryEnabled),
+ icon: Brain,
key: 'memory',
- label: renderActive(t('memory.title'), Boolean(isMemoryEnabled)),
- onClick: handleToggleMemory,
+ label: t('memory.title'),
+ onCheckedChange: handleToggleMemory,
+ type: 'switch',
},
// Web search: simple toggle when 2 options, submenu when 3
...(showProviderSearch
@@ -538,32 +518,57 @@ const PlusAction = memo(() => {
} as ActionDropdownMenuItems[number],
]
: [
+ // Web search toggle — trailing switch; toggle by clicking the switch or the whole row
{
- icon: activeIcon(
- activeSearchOption === 'off' ? GlobeOffIcon : Globe,
- activeSearchOption !== 'off',
- ),
+ checked: activeSearchOption !== 'off',
+ icon: Globe,
key: 'search-toggle',
- label: renderActive(t('search.title'), activeSearchOption !== 'off'),
- onClick: () => handleSelectSearch(activeSearchOption === 'off' ? 'app' : 'off'),
+ label: t('search.title'),
+ onCheckedChange: (checked: boolean) => handleSelectSearch(checked ? 'app' : 'off'),
+ type: 'switch',
} as ActionDropdownMenuItems[number],
]),
+ { type: 'divider' },
+ // Skills (with "Add Skills..." merged in) sits directly under the Web Search divider.
...toolsItems,
+ // Formatting toolbar toggle — trailing switch; toggle by clicking the switch or the whole row
+ {
+ checked: Boolean(showTypoBar),
+ icon: TypeIcon,
+ key: 'typo',
+ label: tEditor('actions.typobar.title'),
+ onCheckedChange: (checked: boolean) => setShowTypoBar(checked),
+ type: 'switch',
+ },
+ // Advanced parameter settings — mirrors ParamsPanelToggle in the agent header.
+ {
+ icon: Settings2Icon,
+ key: 'params',
+ label: renderActive(tSetting('settingModel.params.title'), isParamsPanelActive),
+ onClick: handleToggleParams,
+ },
];
- const knowledgeSectionItems: ActionDropdownMenuItems = enableKnowledgeBase
+ // "Add Attachments..." merges file upload with the knowledge base (libraries / files).
+ // When the knowledge base is disabled there is no submenu, so Upload stays a top-level entry.
+ const attachmentsItems: ActionDropdownMenuItems = enableKnowledgeBase
? [
{
- children: knowledgeItems,
+ children: [
+ ...uploadItems,
+ ...(knowledgeItems.length > 0
+ ? [{ type: 'divider' as const }, ...knowledgeItems]
+ : []),
+ ],
footer: knowledgeFooter,
- icon: activeIcon(LibraryBig, knowledgeEnabledCount > 0),
- key: 'knowledge-base',
- label: renderLabelWithCount(t('knowledgeBase.title'), knowledgeEnabledCount),
+ icon: LibraryBig,
+ key: 'attachments',
+ label: renderLabelWithCount(t('plus.addAttachments'), knowledgeEnabledCount),
} as ActionDropdownMenuItems[number],
]
- : [];
+ : uploadItems;
- return [...uploadItems, ...knowledgeSectionItems, ...capabilityItems];
+ return [...attachmentsItems, ...capabilityItems];
}, [
activeSearchOption,
canUploadImage,
@@ -571,7 +576,6 @@ const PlusAction = memo(() => {
editor,
enableFC,
enableKnowledgeBase,
- handleOpenTools,
handleSelectSearch,
handleToggleMemory,
handleToggleParams,
diff --git a/src/features/ChatInput/ActionBar/Tools/useControls.tsx b/src/features/ChatInput/ActionBar/Tools/useControls.tsx
index 7c4431ab4e..2e1b933fa8 100644
--- a/src/features/ChatInput/ActionBar/Tools/useControls.tsx
+++ b/src/features/ChatInput/ActionBar/Tools/useControls.tsx
@@ -20,6 +20,7 @@ import {
Package,
Pin,
Settings,
+ Store,
Trash2,
Wrench,
Zap,
@@ -30,6 +31,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import DevModal from '@/features/PluginDevModal';
+import { createSkillStoreModal } from '@/features/SkillStore';
import { useCheckPluginsIsInstalled } from '@/hooks/useCheckPluginsIsInstalled';
import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins';
import { useAgentStore } from '@/store/agent';
@@ -347,7 +349,6 @@ const styles = createStaticStyles(({ css }) => ({
width: 24px;
height: 24px;
- margin-inline-start: auto;
padding: 0;
border: 0;
border-radius: 6px;
@@ -1279,20 +1280,43 @@ export const useControls = ({ closeDropdown }: { closeDropdown?: () => void } =
{allAutoItems.length}
-
-
-
+
+
+
+
+
+
+
+
) : undefined;
diff --git a/src/features/ChatInput/ActionBar/Upload/index.tsx b/src/features/ChatInput/ActionBar/Upload/index.tsx
index 24d3b91c53..4c543c0688 100644
--- a/src/features/ChatInput/ActionBar/Upload/index.tsx
+++ b/src/features/ChatInput/ActionBar/Upload/index.tsx
@@ -36,6 +36,11 @@ const hotArea = css`
}
`;
+// Keep every row's leading icon the same width. The menu's icon slot sizes to its
+// content, so a larger file-type icon next to a smaller line icon would widen that
+// slot and push its label out of alignment with the upload / "view more" rows.
+const MENU_ICON_SIZE = 20;
+
const FileUpload = memo(() => {
const { t } = useTranslation('chat');
@@ -76,7 +81,7 @@ const FileUpload = memo(() => {
{
closeOnClick: false,
disabled: !canUploadImage,
- icon: ImageUp,
+ icon: ,
key: 'upload-image',
label: canUploadImage ? (
{
},
{
closeOnClick: false,
- icon: FileUp,
+ icon: ,
key: 'upload-file',
label: (
{
},
{
closeOnClick: false,
- icon: FolderUp,
+ icon: ,
key: 'upload-folder',
label: (
{
children: [
// first the files
...files.map((item) => ({
- icon: ,
+ icon: ,
key: item.id,
label: (
{
// then the knowledge bases
...knowledgeBases.map((item) => ({
- icon: ,
+ icon: ,
key: item.id,
label: (
{
},
{
extra: ,
- icon: LibraryBig,
+ icon: ,
key: 'knowledge-base-store',
label: t('knowledgeBase.viewMore'),
onClick: () => {
diff --git a/src/features/ChatInput/ActionBar/components/ActionDropdown.tsx b/src/features/ChatInput/ActionBar/components/ActionDropdown.tsx
index d3226e919f..5882444c32 100644
--- a/src/features/ChatInput/ActionBar/components/ActionDropdown.tsx
+++ b/src/features/ChatInput/ActionBar/components/ActionDropdown.tsx
@@ -1,8 +1,10 @@
'use client';
import {
+ type BaseMenuItemType,
type DropdownMenuPopupProps,
type DropdownMenuProps,
+ type MenuInfo,
type MenuItemType,
type MenuProps,
type PopoverTrigger,
@@ -95,7 +97,12 @@ const SubmenuScrollStyle = createGlobalStyle`
export type ActionDropdownMenuItem = MenuItemType;
-export type ActionDropdownMenuItems = MenuProps['items'];
+/**
+ * `renderDropdownMenuItems` accepts Base UI's full item union (`BaseMenuItemType`),
+ * which is wider than antd's `MenuProps['items']` — it also covers `type: 'switch'`
+ * and `type: 'checkbox'` items. Use it here so callers can declare those directly.
+ */
+export type ActionDropdownMenuItems = BaseMenuItemType[];
type ActionDropdownMenu = Omit<
Pick, 'className' | 'onClick' | 'style'>,
@@ -199,29 +206,40 @@ const ActionDropdown = memo(
children: item.children ? decorateMenuItems(item.children) : item.children,
};
}
+ // Switch / checkbox items are self-contained: they toggle via `onCheckedChange`,
+ // and Base UI already wires up "click the row or the control" for them. Pass them
+ // through untouched instead of wrapping their click handler.
+ if ('type' in item && (item.type === 'switch' || item.type === 'checkbox')) {
+ return item;
+ }
- if ('children' in item && item.children) {
- const originalOnOpenChange = (item as { onOpenChange?: unknown }).onOpenChange as
- | ((open: boolean, details: unknown) => void)
- | undefined;
+ // Any item carrying a `children` key is a submenu (children may be optional);
+ // route them all here so plain items never inherit a submenu's click signature.
+ if ('children' in item) {
return {
...item,
- children: decorateMenuItems(item.children),
+ children: item.children ? decorateMenuItems(item.children) : item.children,
type: 'submenu',
- };
+ // `children` is re-widened to the full item union; cast back to satisfy
+ // the mixed rc-menu / Base UI submenu types in `BaseMenuItemType`.
+ } as BaseMenuItemType;
}
- const itemOnClick = 'onClick' in item ? item.onClick : undefined;
- const closeOnClick = 'closeOnClick' in item ? item.closeOnClick : undefined;
+ // Submenus are handled above; everything else is a plain menu item. Base UI's
+ // types keep optional-`children` submenu members in scope here, so narrow to the
+ // plain-item type before wrapping the click handler.
+ const menuItem = item as ActionDropdownMenuItem;
+ const itemOnClick = menuItem.onClick;
+ const closeOnClick = menuItem.closeOnClick;
const keepOpenOnClick = closeOnClick === false;
- const itemLabel = 'label' in item ? item.label : undefined;
+ const itemLabel = menuItem.label;
const shouldKeepOpen = isValidElement(itemLabel);
const resolvedCloseOnClick = closeOnClick ?? (shouldKeepOpen ? false : undefined);
return {
- ...item,
+ ...menuItem,
...(resolvedCloseOnClick !== undefined ? { closeOnClick: resolvedCloseOnClick } : null),
- onClick: (info) => {
+ onClick: (info: MenuInfo) => {
if (keepOpenOnClick) {
info.domEvent.stopPropagation();
menu.onClick?.(info);
diff --git a/src/locales/default/chat.ts b/src/locales/default/chat.ts
index bdf47e441a..b7759afcbf 100644
--- a/src/locales/default/chat.ts
+++ b/src/locales/default/chat.ts
@@ -469,6 +469,7 @@ export default {
'plus.search.modelSearchDesc': 'May cause unexpected behavior when enabled, not recommended.',
'plus.search.off': 'Off',
'plus.search.offDesc': '',
+ 'plus.addAttachments': 'Attachments',
'plus.addSkills': 'Add Skills...',
'plus.title': 'Add',
'plus.tooltip': 'Add files, skills, and more context...',
diff --git a/src/locales/default/editor.ts b/src/locales/default/editor.ts
index 143074e2cf..4cefef0eb8 100644
--- a/src/locales/default/editor.ts
+++ b/src/locales/default/editor.ts
@@ -3,6 +3,7 @@ export default {
'actions.expand.on': 'Expand',
'actions.typobar.off': 'Hide formatting toolbar',
'actions.typobar.on': 'Show formatting toolbar',
+ 'actions.typobar.title': 'Formatting Tool',
'autoSave.latest': 'Latest version loaded',
'autoSave.saved': 'Saved',
'autoSave.saving': 'Auto-saving...',