From f45fe05f338cad1d393f9fe4df94dba78642dd53 Mon Sep 17 00:00:00 2001 From: Champii Date: Mon, 8 Jun 2026 11:10:32 -0500 Subject: [PATCH] feat: add file editor vim mode (#1437) Co-authored-by: Bohdan Triapitsyn --- README.md | 2 +- bun.lock | 3 ++ packages/ui/package.json | 1 + .../sections/openchamber/OpenChamberPage.tsx | 1 + .../openchamber/OpenChamberVisualSettings.tsx | 48 ++++++++++++++++- .../ui/src/components/ui/CodeMirrorEditor.tsx | 23 ++++++++ .../ui/src/components/views/FilesView.tsx | 31 ++++++----- .../ui/src/lib/codemirror/flexokiTheme.ts | 14 +++++ .../ui/src/lib/codemirror/vimModeExtension.ts | 52 +++++++++++++++++++ .../ui/src/lib/i18n/messages/en.settings.ts | 3 ++ .../ui/src/lib/i18n/messages/es.settings.ts | 3 ++ .../ui/src/lib/i18n/messages/ko.settings.ts | 3 ++ .../ui/src/lib/i18n/messages/pl.settings.ts | 3 ++ .../src/lib/i18n/messages/pt-BR.settings.ts | 3 ++ .../ui/src/lib/i18n/messages/uk.settings.ts | 3 ++ .../src/lib/i18n/messages/zh-CN.settings.ts | 3 ++ .../src/lib/i18n/messages/zh-TW.settings.ts | 3 ++ packages/ui/src/stores/useUIStore.ts | 15 ++++++ 18 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 packages/ui/src/lib/codemirror/vimModeExtension.ts diff --git a/README.md b/README.md index 339fb124..65561c96 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ chown -R 1000:1000 data/
Files, Diff & Terminal -- Workspace file browser with inline editing, syntax highlighting, and markdown preview +- Workspace file browser with inline editing, syntax highlighting, Vim mode, and markdown preview - Beautiful diff viewer with stacked/inline modes, lazy loading for large changesets - Integrated terminal with per-directory sessions, tabbed interface, and stable heavy-output performance - Clickable file paths in messages - jump to exact line locations diff --git a/bun.lock b/bun.lock index 8c911f5e..bb60740a 100644 --- a/bun.lock +++ b/bun.lock @@ -147,6 +147,7 @@ "@lezer/highlight": "^1.2.3", "@opencode-ai/sdk": "^1.16.0", "@pierre/diffs": "1.1.0-beta.13", + "@replit/codemirror-vim": "^6.3.0", "@simplewebauthn/browser": "13.3.0", "@tanstack/react-virtual": "^3.13.18", "@types/react-syntax-highlighter": "^15.5.13", @@ -1092,6 +1093,8 @@ "@remixicon/react": ["@remixicon/react@4.9.0", "", { "peerDependencies": { "react": ">=18.2.0" } }, "sha512-5/jLDD4DtKxH2B4QVXTobvV1C2uL8ab9D5yAYNtFt+w80O0Ys1xFOrspqROL3fjrZi+7ElFUWE37hBfaAl6U+Q=="], + "@replit/codemirror-vim": ["@replit/codemirror-vim@6.3.0", "", { "peerDependencies": { "@codemirror/commands": "6.x.x", "@codemirror/language": "6.x.x", "@codemirror/search": "6.x.x", "@codemirror/state": "6.x.x", "@codemirror/view": "6.x.x" } }, "sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], "@rollup/plugin-babel": ["@rollup/plugin-babel@5.3.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", "rollup": "^1.20.0||^2.0.0" }, "optionalPeers": ["@types/babel__core"] }, "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q=="], diff --git a/packages/ui/package.json b/packages/ui/package.json index 833c99ee..3639c413 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -42,6 +42,7 @@ "@lezer/highlight": "^1.2.3", "@opencode-ai/sdk": "^1.16.0", "@pierre/diffs": "1.1.0-beta.13", + "@replit/codemirror-vim": "^6.3.0", "@simplewebauthn/browser": "13.3.0", "@tanstack/react-virtual": "^3.13.18", "@types/react-syntax-highlighter": "^15.5.13", diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx index 4d2657ea..ed24bd57 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx @@ -133,6 +133,7 @@ const VisualSectionContent: React.FC = () => { ...(!isVSCode ? ['weekStart' as const] : []), 'fontSize', 'terminalFontSize', + 'fileEditorKeymap', 'spacing', 'inputBarOffset', ...(!isVSCode ? ['terminalQuickKeys' as const] : []), diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index 05407491..44608ce7 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -247,7 +247,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain' return mode === 'markdown' ? 'markdown' : 'plain'; }; -export type VisibleSetting = 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'stickyUserHeader' | 'wideChatLayout' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'queueMode' | 'terminalQuickKeys' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage'; +export type VisibleSetting = 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'stickyUserHeader' | 'wideChatLayout' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'queueMode' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage'; interface OpenChamberVisualSettingsProps { /** Which settings to show. If undefined, shows all. */ @@ -297,6 +297,8 @@ export const OpenChamberVisualSettings: React.FC const setDiffViewMode = useUIStore(state => state.setDiffViewMode); const showTerminalQuickKeysOnDesktop = useUIStore(state => state.showTerminalQuickKeysOnDesktop); const setShowTerminalQuickKeysOnDesktop = useUIStore(state => state.setShowTerminalQuickKeysOnDesktop); + const fileEditorKeymap = useUIStore(state => state.fileEditorKeymap); + const setFileEditorKeymap = useUIStore(state => state.setFileEditorKeymap); const queueModeEnabled = useMessageQueueStore(state => state.queueModeEnabled); const setQueueMode = useMessageQueueStore(state => state.setQueueMode); const persistChatDraft = useUIStore(state => state.persistChatDraft); @@ -516,7 +518,7 @@ export const OpenChamberVisualSettings: React.FC ? hasLocalizationSettings : (shouldShow('theme') || showMobileLayoutSetting || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart')); const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('inputBarOffset'); - const hasNavigationSettings = shouldShow('terminalQuickKeys') && !isMobile; + const hasNavigationSettings = (shouldShow('terminalQuickKeys') && !isMobile) || shouldShow('fileEditorKeymap'); const hasBehaviorSettings = shouldShow('mermaidRendering') || shouldShow('userMessageRendering') || shouldShow('chatRenderMode') @@ -1211,6 +1213,48 @@ export const OpenChamberVisualSettings: React.FC

{t('settings.openchamber.visual.section.navigation')}

+ {shouldShow('fileEditorKeymap') && ( +
+ + {t('settings.openchamber.visual.field.fileEditorKeymap')} + +
+ {(['default', 'vim'] as const).map((keymap) => { + const selected = fileEditorKeymap === keymap; + const labelText = t(`settings.openchamber.visual.option.fileEditorKeymap.${keymap}`); + return ( + + ); + })} +
+
+ )} {shouldShow('terminalQuickKeys') && !isMobile && (
void; + vimMode?: boolean; }; const lineNumbersCompartment = new Compartment(); @@ -149,6 +151,7 @@ const externalExtensionsCompartment = new Compartment(); const highlightLinesCompartment = new Compartment(); const blockWidgetsCompartment = new Compartment(); const searchCompartment = new Compartment(); +const vimCompartment = new Compartment(); const toViewKeyBindings = (bindings: readonly unknown[]): readonly KeyBinding[] => { return bindings as readonly KeyBinding[]; @@ -265,6 +268,7 @@ export function CodeMirrorEditor({ enableSearch, searchOpen, onSearchOpenChange, + vimMode, }: CodeMirrorEditorProps) { const hostRef = React.useRef(null); const viewRef = React.useRef(null); @@ -274,6 +278,7 @@ export function CodeMirrorEditor({ const onViewDestroyRef = React.useRef(onViewDestroy); const onSearchOpenChangeRef = React.useRef(onSearchOpenChange); const blockWidgetsRef = React.useRef(blockWidgets); + const vimModeRef = React.useRef(vimMode); // Scoped map for widget containers to avoid global collisions and memory leaks const widgetContainersRef = React.useRef(new Map()); @@ -367,6 +372,7 @@ export function CodeMirrorEditor({ lineNumbersCompartment.of(lineNumbers(lineNumbersConfig)), history(), indentUnit.of(' '), + vimCompartment.of(createVimModeExtensions(vimMode)), keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]), EditorView.updateListener.of((update) => { syncEditorCssVars(update.view); @@ -449,6 +455,23 @@ export function CodeMirrorEditor({ }); }, [extensions, highlightLines, lineNumbersConfig, readOnly, blockWidgets, enableSearch, syncEditorCssVars, syncPortalWidgets]); + React.useEffect(() => { + const view = viewRef.current; + if (!view) { + vimModeRef.current = vimMode; + return; + } + + if (vimModeRef.current === vimMode) { + return; + } + + vimModeRef.current = vimMode; + view.dispatch({ + effects: vimCompartment.reconfigure(createVimModeExtensions(vimMode)), + }); + }, [vimMode]); + React.useEffect(() => { const view = viewRef.current; if (!view || enableSearch === false) { diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index d65b1321..2014df0f 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -906,10 +906,11 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const setMainTabGuard = useUIStore((state) => state.setMainTabGuard); const pendingFileNavigation = useUIStore((state) => state.pendingFileNavigation); const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation); - const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath); - const setPendingFileFocusPath = useUIStore((state) => state.setPendingFileFocusPath); - const shortcutOverrides = useUIStore((state) => state.shortcutOverrides); - const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview); + const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath); + const setPendingFileFocusPath = useUIStore((state) => state.setPendingFileFocusPath); + const shortcutOverrides = useUIStore((state) => state.shortcutOverrides); + const fileEditorKeymap = useUIStore((state) => state.fileEditorKeymap); + const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview); // Global mouseup to end drag selection React.useEffect(() => { @@ -3555,11 +3556,12 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { >
{ editorViewRef.current = view; @@ -3852,11 +3854,12 @@ export const FilesView: React.FC = ({ mode = 'full' }) => {
{ editorViewRef.current = view; window.requestAnimationFrame(() => { diff --git a/packages/ui/src/lib/codemirror/flexokiTheme.ts b/packages/ui/src/lib/codemirror/flexokiTheme.ts index 613562d4..8fb2bd6c 100644 --- a/packages/ui/src/lib/codemirror/flexokiTheme.ts +++ b/packages/ui/src/lib/codemirror/flexokiTheme.ts @@ -143,6 +143,20 @@ export function createFlexokiCodeMirrorTheme(theme: Theme): Extension { left: 'auto', width: 'auto', }, + '.cm-panels-bottom': { + position: 'static' as const, + width: '100%', + pointerEvents: 'auto', + }, + '.cm-panels-bottom .cm-vim-panel': { + borderTop: `1px solid ${theme.colors.interactive.border}`, + backgroundColor: theme.colors.surface.elevated, + color: theme.colors.surface.mutedForeground, + fontFamily: 'var(--font-mono)', + fontSize: '12px', + minHeight: '22px', + padding: '2px 8px', + }, /* ── Floating search panel ── */ '.cm-panel.cm-search': { diff --git a/packages/ui/src/lib/codemirror/vimModeExtension.ts b/packages/ui/src/lib/codemirror/vimModeExtension.ts new file mode 100644 index 00000000..b1ee328a --- /dev/null +++ b/packages/ui/src/lib/codemirror/vimModeExtension.ts @@ -0,0 +1,52 @@ +import type { Extension } from '@codemirror/state'; +import type { EditorView, Panel } from '@codemirror/view'; +import { drawSelection, showPanel } from '@codemirror/view'; +import { getCM, vim } from '@replit/codemirror-vim'; + +type VimStatusPlugin = { + updateStatus: () => void; +}; + +const isVimStatusPlugin = (value: unknown): value is VimStatusPlugin => { + return ( + value !== null + && typeof value === 'object' + && 'updateStatus' in value + && typeof value.updateStatus === 'function' + ); +}; + +const bottomVimStatusPanel = (view: EditorView): Panel => { + const dom = document.createElement('div'); + dom.className = 'cm-vim-panel'; + + const cm = getCM(view); + if (!cm) { + return { top: false, dom }; + } + + cm.state.statusbar = dom; + const vimPlugin: unknown = cm.state.vimPlugin; + // The Vim package exposes only a top status panel; reuse its updater with a bottom panel. + if (isVimStatusPlugin(vimPlugin)) { + vimPlugin.updateStatus(); + } + + return { + top: false, + dom, + destroy() { + if (cm.state.statusbar === dom) { + cm.state.statusbar = null; + } + }, + }; +}; + +export function createVimModeExtensions(enabled: boolean | undefined): Extension[] { + if (!enabled) { + return []; + } + + return [vim(), showPanel.of(bottomVimStatusPanel), drawSelection()]; +} diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index a520c532..804094d4 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -1605,6 +1605,9 @@ export const settingsDict = { 'settings.openchamber.visual.field.terminalQuickKeysAria': 'Terminal quick keys', 'settings.openchamber.visual.field.terminalQuickKeys': 'Terminal Quick Keys', 'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Show Esc, Ctrl, Arrows in terminal view', + 'settings.openchamber.visual.field.fileEditorKeymap': 'File editor keymap', + 'settings.openchamber.visual.option.fileEditorKeymap.default': 'Default', + 'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim', 'settings.openchamber.visual.field.activityDefaultModeAria': 'Activity default mode: {option}', 'settings.openchamber.visual.field.showExpandedBashToolsAria': 'Show expanded bash tools', 'settings.openchamber.visual.field.showExpandedEditToolsAria': 'Show expanded edit tools', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index cf97c42d..e8cdaf6c 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -1572,6 +1572,9 @@ export const settingsDict = { "settings.openchamber.visual.field.terminalQuickKeysAria": "Teclas rápidas del terminal", "settings.openchamber.visual.field.terminalQuickKeys": "Teclas rápidas del terminal", "settings.openchamber.visual.field.terminalQuickKeysTooltip": "Mostrar Esc, Ctrl y flechas en la vista del terminal", + "settings.openchamber.visual.field.fileEditorKeymap": "Mapa de teclas del editor de archivos", + "settings.openchamber.visual.option.fileEditorKeymap.default": "Predeterminado", + "settings.openchamber.visual.option.fileEditorKeymap.vim": "Vim", "settings.openchamber.visual.field.activityDefaultModeAria": "Modo predeterminado de actividad: {option}", "settings.openchamber.visual.field.showExpandedBashToolsAria": "Mostrar herramientas de Bash expandidas", "settings.openchamber.visual.field.showExpandedEditToolsAria": "Mostrar herramientas de edición expandidas", diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index 61fc1ea4..5efd9569 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -1572,6 +1572,9 @@ export const settingsDict = { 'settings.openchamber.visual.field.terminalQuickKeysAria': '터미널 빠른 키', 'settings.openchamber.visual.field.terminalQuickKeys': '터미널 빠른 키', 'settings.openchamber.visual.field.terminalQuickKeysTooltip': '터미널 보기에서 Esc, Ctrl, 화살표를 표시합니다', + 'settings.openchamber.visual.field.fileEditorKeymap': '파일 편집기 키맵', + 'settings.openchamber.visual.option.fileEditorKeymap.default': '기본값', + 'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim', 'settings.openchamber.visual.field.activityDefaultModeAria': 'Activity 기본 모드: {option}', 'settings.openchamber.visual.field.showExpandedBashToolsAria': '확장된 Bash 도구 표시', 'settings.openchamber.visual.field.showExpandedEditToolsAria': '확장된 편집 도구 표시', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index 58f3f1ab..b417b792 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -949,6 +949,9 @@ export const settingsDict = { 'settings.openchamber.visual.field.terminalQuickKeys': 'Szybkie klawisze terminala', 'settings.openchamber.visual.field.terminalQuickKeysAria': 'Szybkie klawisze terminala', 'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Pokaż Esc, Ctrl i strzałki w widoku terminala', + 'settings.openchamber.visual.field.fileEditorKeymap': 'Mapa klawiszy edytora plików', + 'settings.openchamber.visual.option.fileEditorKeymap.default': 'Domyślna', + 'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim', 'settings.openchamber.visual.field.themeImportInfoAria': 'Informacje o imporcie motywów', 'settings.openchamber.visual.field.themeImportInfoTooltip': 'Importuj własne motywy z ~/.config/openchamber/themes/', 'settings.openchamber.visual.field.timeFormat': 'Format czasu', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index 4b68286b..45844c38 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1572,6 +1572,9 @@ export const settingsDict = { "settings.openchamber.visual.field.terminalQuickKeysAria": "Teclas rápidas do terminal", "settings.openchamber.visual.field.terminalQuickKeys": "Teclas rápidas do terminal", "settings.openchamber.visual.field.terminalQuickKeysTooltip": "Mostrar Esc, Ctrl e flechas na vista do terminal", + "settings.openchamber.visual.field.fileEditorKeymap": "Mapa de teclas do editor de arquivos", + "settings.openchamber.visual.option.fileEditorKeymap.default": "Padrão", + "settings.openchamber.visual.option.fileEditorKeymap.vim": "Vim", "settings.openchamber.visual.field.activityDefaultModeAria": "Modo padrão de atividade: {option}", "settings.openchamber.visual.field.showExpandedBashToolsAria": "Mostrar ferramentas de Bash expandidas", "settings.openchamber.visual.field.showExpandedEditToolsAria": "Mostrar ferramentas de edição expandidas", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 1d3b957a..ef7f5100 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1572,6 +1572,9 @@ export const settingsDict = { "settings.openchamber.visual.field.terminalQuickKeysAria": "Швидкі клавіші терміналу", "settings.openchamber.visual.field.terminalQuickKeys": "Швидкі клавіші терміналу", "settings.openchamber.visual.field.terminalQuickKeysTooltip": "Показати Esc, Ctrl, стрілки в поданні терміналу", + "settings.openchamber.visual.field.fileEditorKeymap": "Розкладка клавіш редактора файлів", + "settings.openchamber.visual.option.fileEditorKeymap.default": "Типова", + "settings.openchamber.visual.option.fileEditorKeymap.vim": "Vim", "settings.openchamber.visual.field.activityDefaultModeAria": "Типовий режим активності: {option}", "settings.openchamber.visual.field.showExpandedBashToolsAria": "Показати розширені інструменти bash", "settings.openchamber.visual.field.showExpandedEditToolsAria": "Показати розширені інструменти редагування", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index 4410a6c7..264bd711 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1572,6 +1572,9 @@ export const settingsDict = { 'settings.openchamber.visual.field.terminalQuickKeysAria': '终端快捷键', 'settings.openchamber.visual.field.terminalQuickKeys': '终端快捷键', 'settings.openchamber.visual.field.terminalQuickKeysTooltip': '在终端视图显示 Esc、Ctrl、方向键', + 'settings.openchamber.visual.field.fileEditorKeymap': '文件编辑器键位映射', + 'settings.openchamber.visual.option.fileEditorKeymap.default': '默认', + 'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim', 'settings.openchamber.visual.field.activityDefaultModeAria': '活动默认模式:{option}', 'settings.openchamber.visual.field.showExpandedBashToolsAria': '默认展开 Bash 工具', 'settings.openchamber.visual.field.showExpandedEditToolsAria': '默认展开编辑工具', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index 387a49a6..47c91da5 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -1493,6 +1493,9 @@ 'settings.openchamber.visual.field.terminalQuickKeysAria': '終端機快速鍵', 'settings.openchamber.visual.field.terminalQuickKeys': '終端機快速鍵', 'settings.openchamber.visual.field.terminalQuickKeysTooltip': '在終端機檢視顯示 Esc、Ctrl、方向鍵', + 'settings.openchamber.visual.field.fileEditorKeymap': '檔案編輯器鍵位映射', + 'settings.openchamber.visual.option.fileEditorKeymap.default': '預設', + 'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim', 'settings.openchamber.visual.field.activityDefaultModeAria': '活動預設模式:{option}', 'settings.openchamber.visual.field.showExpandedBashToolsAria': '預設展開 Bash 工具', 'settings.openchamber.visual.field.showExpandedEditToolsAria': '預設展開編輯工具', diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index a1b39aec..878b8fe5 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -19,6 +19,11 @@ export type ActivityRenderMode = 'collapsed' | 'summary'; export type SessionRetentionAction = 'archive' | 'delete'; export type TimeFormatPreference = 'auto' | '12h' | '24h'; export type WeekStartPreference = 'auto' | 'sunday' | 'monday'; +export type FileEditorKeymap = 'default' | 'vim'; + +function normalizeFileEditorKeymap(value: unknown): FileEditorKeymap { + return value === 'vim' ? 'vim' : 'default'; +} type ContextPanelTab = { id: string; @@ -622,6 +627,7 @@ interface UIStore { isExpandedInput: boolean; reportUsage: boolean; shortcutOverrides: Record; + fileEditorKeymap: FileEditorKeymap; setTheme: (theme: 'light' | 'dark' | 'system') => void; toggleSidebar: () => void; @@ -771,6 +777,7 @@ interface UIStore { setShortcutOverride: (actionId: string, combo: ShortcutCombo) => void; clearShortcutOverride: (actionId: string) => void; resetAllShortcutOverrides: () => void; + setFileEditorKeymap: (value: FileEditorKeymap) => void; } @@ -899,6 +906,7 @@ export const useUIStore = create()( isExpandedInput: false, reportUsage: true, shortcutOverrides: {}, + fileEditorKeymap: 'default', setTheme: (theme) => { set({ theme }); @@ -2050,6 +2058,10 @@ export const useUIStore = create()( set({ shortcutOverrides: {} }); }, + setFileEditorKeymap: (value) => { + set({ fileEditorKeymap: normalizeFileEditorKeymap(value) }); + }, + toggleExpandedInput: () => { set((state) => ({ isExpandedInput: !state.isExpandedInput })); }, @@ -2149,6 +2161,8 @@ export const useUIStore = create()( } } + state.fileEditorKeymap = normalizeFileEditorKeymap(state.fileEditorKeymap); + return state; }, partialize: (state) => ({ @@ -2230,6 +2244,7 @@ export const useUIStore = create()( isMobileSessionStatusBarCollapsed: state.isMobileSessionStatusBarCollapsed, mobileSessionFilterProjectId: state.mobileSessionFilterProjectId, shortcutOverrides: state.shortcutOverrides, + fileEditorKeymap: state.fileEditorKeymap, }) } ),