fix(ui): restore composer focus after CodeMirror migration
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { focusChatInput } from './composer/editor/dom';
|
||||||
import type { EditPermissionMode } from '@/stores/types/sessionTypes';
|
import type { EditPermissionMode } from '@/stores/types/sessionTypes';
|
||||||
import type { ModelMetadata } from '@/types';
|
import type { ModelMetadata } from '@/types';
|
||||||
import {
|
import {
|
||||||
@@ -478,10 +479,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
|
|
||||||
// Restore focus to chat input when model selector closes
|
// Restore focus to chat input when model selector closes
|
||||||
if (wasOpen && !isCompact) {
|
if (wasOpen && !isCompact) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(focusChatInput);
|
||||||
const textarea = document.querySelector<HTMLTextAreaElement>('textarea[data-chat-input="true"]');
|
|
||||||
textarea?.focus();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isModelSelectorOpen, isCompact]);
|
}, [isModelSelectorOpen, isCompact]);
|
||||||
@@ -492,10 +490,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
if (!isAgentSelectorOpen) {
|
if (!isAgentSelectorOpen) {
|
||||||
setAgentSearchQuery('');
|
setAgentSearchQuery('');
|
||||||
if (!isCompact) {
|
if (!isCompact) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(focusChatInput);
|
||||||
const textarea = document.querySelector<HTMLTextAreaElement>('textarea[data-chat-input="true"]');
|
|
||||||
textarea?.focus();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isAgentSelectorOpen, isCompact]);
|
}, [isAgentSelectorOpen, isCompact]);
|
||||||
@@ -1264,10 +1259,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
closeMobilePanel();
|
closeMobilePanel();
|
||||||
}
|
}
|
||||||
// Restore focus to chat input after model selection.
|
// Restore focus to chat input after model selection.
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(focusChatInput);
|
||||||
const textarea = document.querySelector<HTMLTextAreaElement>('textarea[data-chat-input="true"]');
|
|
||||||
textarea?.focus();
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ModelControls] Handle model change error:', error);
|
console.error('[ModelControls] Handle model change error:', error);
|
||||||
}
|
}
|
||||||
@@ -1605,13 +1597,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusMobileComposer = () => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const textarea = document.querySelector<HTMLTextAreaElement>('textarea[data-chat-input="true"]');
|
|
||||||
textarea?.focus();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMobileModelApply = (providerId: string, modelId: string, variant: string | undefined) => {
|
const handleMobileModelApply = (providerId: string, modelId: string, variant: string | undefined) => {
|
||||||
const result = applyModelSelectionWithVariant(providerId, modelId, variant);
|
const result = applyModelSelectionWithVariant(providerId, modelId, variant);
|
||||||
if (result !== 'applied') {
|
if (result !== 'applied') {
|
||||||
@@ -1625,7 +1610,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
|
|
||||||
setExpandedMobileModelKey(null);
|
setExpandedMobileModelKey(null);
|
||||||
closeMobilePanel();
|
closeMobilePanel();
|
||||||
focusMobileComposer();
|
requestAnimationFrame(focusChatInput);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openMobileVariantOverflow = (providerId: string, modelId: string) => {
|
const openMobileVariantOverflow = (providerId: string, modelId: string) => {
|
||||||
@@ -1961,10 +1946,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
closeMobilePanel();
|
closeMobilePanel();
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(focusChatInput);
|
||||||
const textarea = document.querySelector<HTMLTextAreaElement>('textarea[data-chat-input="true"]');
|
|
||||||
textarea?.focus();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -504,6 +504,7 @@ export const ComposerEditor = React.forwardRef<ComposerEditorHandle, ComposerEdi
|
|||||||
<div
|
<div
|
||||||
ref={hostRef}
|
ref={hostRef}
|
||||||
data-testid={props['data-testid']}
|
data-testid={props['data-testid']}
|
||||||
|
data-chat-input="true"
|
||||||
onMouseDown={handleHostMouseDown}
|
onMouseDown={handleHostMouseDown}
|
||||||
className={cn(
|
className={cn(
|
||||||
'composer-editor w-full',
|
'composer-editor w-full',
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { afterEach, expect, test } from 'bun:test';
|
||||||
|
|
||||||
|
import { focusChatInput } from '../dom';
|
||||||
|
|
||||||
|
const originalDocument = globalThis.document;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.document = originalDocument;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('focuses the CodeMirror chat input content', () => {
|
||||||
|
let selector = '';
|
||||||
|
let focused = false;
|
||||||
|
globalThis.document = {
|
||||||
|
querySelector: (value: string) => {
|
||||||
|
selector = value;
|
||||||
|
return { focus: () => { focused = true; } };
|
||||||
|
},
|
||||||
|
} as unknown as Document;
|
||||||
|
|
||||||
|
focusChatInput();
|
||||||
|
|
||||||
|
expect(selector).toBe('[data-chat-input="true"] .cm-content');
|
||||||
|
expect(focused).toBe(true);
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const CHAT_INPUT_EDITOR_SELECTOR = '[data-chat-input="true"] .cm-content';
|
||||||
|
|
||||||
|
export function focusChatInput(): void {
|
||||||
|
document.querySelector<HTMLElement>(CHAT_INPUT_EDITOR_SELECTOR)?.focus();
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import { readEmbeddedThemeSearchParams } from '@/contexts/theme-embedded-bootstr
|
|||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||||
import { getCycledPrimaryAgentName } from '@/components/chat/mobileControlsUtils';
|
import { getCycledPrimaryAgentName } from '@/components/chat/mobileControlsUtils';
|
||||||
|
import { focusChatInput } from '@/components/chat/composer/editor/dom';
|
||||||
|
|
||||||
export const useKeyboardShortcuts = () => {
|
export const useKeyboardShortcuts = () => {
|
||||||
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
||||||
@@ -218,7 +219,7 @@ export const useKeyboardShortcuts = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isChatInputTarget = (target: EventTarget | null) => {
|
const isChatInputTarget = (target: EventTarget | null) => {
|
||||||
return target instanceof HTMLTextAreaElement && target.getAttribute('data-chat-input') === 'true';
|
return target instanceof Element && Boolean(target.closest('[data-chat-input="true"]'));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (eventMatchesShortcut(e, combo('open_command_palette'))) {
|
if (eventMatchesShortcut(e, combo('open_command_palette'))) {
|
||||||
@@ -355,8 +356,7 @@ export const useKeyboardShortcuts = () => {
|
|||||||
|
|
||||||
if (eventMatchesShortcut(e, combo('focus_input'))) {
|
if (eventMatchesShortcut(e, combo('focus_input'))) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const textarea = document.querySelector<HTMLTextAreaElement>('textarea[data-chat-input="true"]');
|
focusChatInput();
|
||||||
textarea?.focus();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { focusChatInput } from '@/components/chat/composer/editor/dom';
|
||||||
import { canUseElectronDesktopIPC, invokeDesktop } from '@/lib/desktop';
|
import { canUseElectronDesktopIPC, invokeDesktop } from '@/lib/desktop';
|
||||||
import { eventMatchesShortcut, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
import { eventMatchesShortcut, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
@@ -8,11 +9,6 @@ import { useUIStore } from '@/stores/useUIStore';
|
|||||||
import { useSelectionStore } from '@/sync/selection-store';
|
import { useSelectionStore } from '@/sync/selection-store';
|
||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
|
|
||||||
const focusChatInput = () => {
|
|
||||||
const textarea = document.querySelector<HTMLTextAreaElement>('textarea[data-chat-input="true"]');
|
|
||||||
textarea?.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useMiniChatKeyboardShortcuts = () => {
|
export const useMiniChatKeyboardShortcuts = () => {
|
||||||
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
|
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
|
||||||
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
||||||
|
|||||||
@@ -109,22 +109,6 @@ html[data-oc-vibrancy] .oc-vibrancy-pill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Suppress WebKit-specific hover/focus adornments on the chat textarea */
|
|
||||||
textarea[data-chat-input="true"] {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
background: transparent !important;
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea[data-chat-input="true"]:hover,
|
|
||||||
textarea[data-chat-input="true"]:focus,
|
|
||||||
textarea[data-chat-input="true"]:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure interactive controls consistently show pointer cursor */
|
/* Ensure interactive controls consistently show pointer cursor */
|
||||||
:where(
|
:where(
|
||||||
button,
|
button,
|
||||||
@@ -303,12 +287,12 @@ textarea[data-chat-input="true"]:focus-visible {
|
|||||||
color: color-mix(in srgb, var(--syntax-comment) 85%, var(--syntax-foreground) 15%);
|
color: color-mix(in srgb, var(--syntax-comment) 85%, var(--syntax-foreground) 15%);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.vscode-runtime textarea[data-chat-input="true"]::placeholder {
|
:root.vscode-runtime [data-chat-input="true"] .cm-placeholder {
|
||||||
color: color-mix(in srgb, var(--vscode-input-placeholderForeground, var(--muted-foreground)) 65%, transparent);
|
color: color-mix(in srgb, var(--vscode-input-placeholderForeground, var(--muted-foreground)) 65%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Match chat footer background to input in WebKit to avoid dual-tone hover/focus */
|
/* Match chat footer background to input in WebKit to avoid dual-tone hover/focus */
|
||||||
[data-chat-input="true"] + div[data-chat-input-footer="true"] {
|
div[data-chat-input-footer="true"] {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user