fix(ui): improve composer focus, keyboard navigation, and settings (#3376)
* fix(ui): make composer keyboard interactions consistent * docs(settings): refine description visibility guidance --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
1306b1124c
commit
5ae1a949c8
@@ -23,7 +23,7 @@ divs — use the primitives, and extend them (in the shared file) when a new
|
|||||||
shape is genuinely missing.
|
shape is genuinely missing.
|
||||||
|
|
||||||
- Flat hierarchy through spacing and typography; no cards, boxed backgrounds, or row chrome.
|
- Flat hierarchy through spacing and typography; no cards, boxed backgrounds, or row chrome.
|
||||||
- Secondary helper text is hidden behind an info icon (`info` prop); the default view stays quiet.
|
- Secondary helper text is hidden behind an info icon (`info` prop) by default; the default view stays quiet.
|
||||||
- Controls have one standard size (`h-9` / select `size="settings"`) and capped widths — no full-bleed inputs.
|
- Controls have one standard size (`h-9` / select `size="settings"`) and capped widths — no full-bleed inputs.
|
||||||
- Layouts respond to the settings pane width via container queries (`@xl:` / `@3xl:`), never viewport `sm:`/`lg:` breakpoints (the pane is much narrower than the viewport inside the dialog).
|
- Layouts respond to the settings pane width via container queries (`@xl:` / `@3xl:`), never viewport `sm:`/`lg:` breakpoints (the pane is much narrower than the viewport inside the dialog).
|
||||||
- Checkbox/radio state comes before labels; selected states are subtle and never shift layout.
|
- Checkbox/radio state comes before labels; selected states are subtle and never shift layout.
|
||||||
@@ -57,7 +57,8 @@ Do not introduce raw `<Tooltip>`-based info icons, direct Remixicon components,
|
|||||||
|
|
||||||
## Description Policy (info hints)
|
## Description Policy (info hints)
|
||||||
|
|
||||||
- Explanatory prose (what a feature does, when it applies) goes behind the info icon via the `info` prop — never as always-visible `description`.
|
- Explanatory prose goes behind the info icon via the `info` prop by default.
|
||||||
|
- When labels alone cannot explain the differences, consequences, or conditions needed to choose a setting, use a title, a visible description, then checkbox or radio controls. Large-text paste modes and send shortcuts with expanded-composer exceptions need this explanation. Having multiple options or a group title alone does not require a description; see `references/controls.md` for composition.
|
||||||
- Stays visible: security/data-loss warnings, destructive consequences, required syntax/placeholder lists the user reads while typing, dynamic status, empty states, validation errors, active-flow wizard instructions.
|
- Stays visible: security/data-loss warnings, destructive consequences, required syntax/placeholder lists the user reads while typing, dynamic status, empty states, validation errors, active-flow wizard instructions.
|
||||||
- Mixed text: keep the warning sentence visible, move the explanation to `info`.
|
- Mixed text: keep the warning sentence visible, move the explanation to `info`.
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ Dynamic entity rows normally are not indexed. Load `references/search.md` for ex
|
|||||||
## Completion Criteria
|
## Completion Criteria
|
||||||
|
|
||||||
- Built from shared primitives; no ad-hoc page/section/row markup.
|
- Built from shared primitives; no ad-hoc page/section/row markup.
|
||||||
- Explanatory text hidden behind `info`; warnings/syntax/status still visible.
|
- Description placement follows the policy above; warnings/syntax/status remain visible.
|
||||||
- Container-query (`@xl:`/`@3xl:`) responsiveness — no viewport breakpoints in pane content.
|
- Container-query (`@xl:`/`@3xl:`) responsiveness — no viewport breakpoints in pane content.
|
||||||
- Controls use the standard size and width caps; no stretched full-width inputs.
|
- Controls use the standard size and width caps; no stretched full-width inputs.
|
||||||
- Localized visible and accessibility text everywhere.
|
- Localized visible and accessibility text everywhere.
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ cells or when the control is wide; same `info` / `settingsItem` props.
|
|||||||
|
|
||||||
## Boolean
|
## Boolean
|
||||||
|
|
||||||
|
For a self-explanatory enable/disable setting, use only a checkbox and label;
|
||||||
|
no separate group title or description is needed.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<SettingsCheckboxRow
|
<SettingsCheckboxRow
|
||||||
checked={value}
|
checked={value}
|
||||||
@@ -54,10 +57,21 @@ for text that must stay visible (warnings, dynamic status).
|
|||||||
|
|
||||||
## Mutually Exclusive Options
|
## Mutually Exclusive Options
|
||||||
|
|
||||||
|
Use radios for mutually exclusive modes, not independent checkboxes. Keep
|
||||||
|
self-explanatory choices compact; a title or description is not mandatory.
|
||||||
|
|
||||||
|
When the skill's Description Policy calls for a visible explanation, wrap
|
||||||
|
the controls in `SettingsControlGroup`: title, description, then options.
|
||||||
|
Explain the choice once at group level. Use checkbox rows for independent
|
||||||
|
choices and radio options for mutually exclusive choices. Group spacing is
|
||||||
|
defined in `layout.md`.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<SettingsRadioGroup aria-label={t('...group')}>
|
<SettingsControlGroup title={t('...group')} description={t('...description')}>
|
||||||
<SettingsRadioOption selected={…} onSelect={…} label={t('...')} ariaLabel={t('...')} />
|
<SettingsRadioGroup aria-label={t('...group')}>
|
||||||
</SettingsRadioGroup>
|
<SettingsRadioOption selected={…} onSelect={…} label={t('...')} ariaLabel={t('...')} />
|
||||||
|
</SettingsRadioGroup>
|
||||||
|
</SettingsControlGroup>
|
||||||
```
|
```
|
||||||
|
|
||||||
Skip per-option descriptions when labels are self-explanatory. For short
|
Skip per-option descriptions when labels are self-explanatory. For short
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ All primitives and class constants below live in
|
|||||||
| L2 | `SettingsSection` title (`SETTINGS_SECTION_TITLE_CLASS`) | Section |
|
| L2 | `SettingsSection` title (`SETTINGS_SECTION_TITLE_CLASS`) | Section |
|
||||||
| L3 | `SettingsControlGroup` title (`SETTINGS_GROUP_TITLE_CLASS`) | Sub-cluster inside a section |
|
| L3 | `SettingsControlGroup` title (`SETTINGS_GROUP_TITLE_CLASS`) | Sub-cluster inside a section |
|
||||||
| L4 | `SETTINGS_FIELD_LABEL_CLASS` | Field / control labels |
|
| L4 | `SETTINGS_FIELD_LABEL_CLASS` | Field / control labels |
|
||||||
| Helper | `SETTINGS_HELPER_CLASS`, `SETTINGS_DESCRIPTION_CLASS` | Rare visible helper text (most goes behind `info`) |
|
| Helper | `SETTINGS_HELPER_CLASS`, `SETTINGS_DESCRIPTION_CLASS` | Rare visible helper text (most goes behind `info`; see the skill's Description Policy) |
|
||||||
|
|
||||||
## Navigation Placement
|
## Navigation Placement
|
||||||
|
|
||||||
@@ -63,5 +63,6 @@ pattern when touching nav.
|
|||||||
- Sections own vertical rhythm: divider + `py-8` come from `SettingsSection`.
|
- Sections own vertical rhythm: divider + `py-8` come from `SettingsSection`.
|
||||||
- Fields inside a column: `SETTINGS_FIELDS_STACK_CLASS` (`space-y-4`).
|
- Fields inside a column: `SETTINGS_FIELDS_STACK_CLASS` (`space-y-4`).
|
||||||
- Checkbox/radio lists: `SETTINGS_OPTION_STACK_CLASS` (`space-y-1.5`).
|
- Checkbox/radio lists: `SETTINGS_OPTION_STACK_CLASS` (`space-y-1.5`).
|
||||||
|
- Groups requiring a title and visible description: separate them from preceding controls with `space-y-6` on the parent. Keep simple checkbox/radio lists compact. The title sits closer to its own description and controls than to the preceding group; use `SettingsControlGroup`'s internal spacing.
|
||||||
- Two-column areas: `SettingsTwoColumn` (`@3xl:grid-cols-2`); use `SettingsStackedField` inside cells (a `SettingsFieldRow` overflows half-width columns).
|
- Two-column areas: `SettingsTwoColumn` (`@3xl:grid-cols-2`); use `SettingsStackedField` inside cells (a `SettingsFieldRow` overflows half-width columns).
|
||||||
- No elevated backgrounds, rounded rows, or hover fills without explicit UX value.
|
- No elevated backgrounds, rounded rows, or hover fills without explicit UX value.
|
||||||
|
|||||||
@@ -61,6 +61,14 @@ Use `Button` from `packages/ui/src/components/ui/button.tsx`.
|
|||||||
|
|
||||||
Do not hardcode button height/padding when a size variant exists. Do not recreate selection/destructive styling with ad-hoc classes.
|
Do not hardcode button height/padding when a size variant exists. Do not recreate selection/destructive styling with ad-hoc classes.
|
||||||
|
|
||||||
|
## Keyboard Navigation Contract
|
||||||
|
|
||||||
|
- Menus, selects, and autocomplete pickers with ArrowDown/ArrowUp navigation must also support Ctrl+N/Ctrl+P, including submenus and searchable lists.
|
||||||
|
- Keep this behavior in shared components so callers inherit it. Use the keyboard mapping in `packages/ui/src/components/ui/dropdown-navigation.ts`; feature code must not duplicate key detection.
|
||||||
|
- Lists that own their active option or stop keyboard propagation must call the shared navigation helper at their own event boundary. Wrapping a custom list in a dropdown does not guarantee that its navigation events reach the wrapper.
|
||||||
|
- Route both key pairs through the same selection logic, preserving disabled-item skipping, boundary or wrap behavior, highlight, and scroll visibility. Consume each navigation event once, only while the menu or picker is active; preserve IME text entry and other modifier chords.
|
||||||
|
- Verify Ctrl+N/P alongside arrow keys in the real component, including search-input focus, submenus, and closed state. A key-mapping unit test alone does not verify event propagation or focus behavior.
|
||||||
|
|
||||||
## Icon Contract
|
## Icon Contract
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@@ -82,6 +90,7 @@ For any other technique, load `performance-engineering` and `scripts/perf/DOCUME
|
|||||||
- Animations are limited to `transform` and `opacity`, or their cost was measured and accepted.
|
- Animations are limited to `transform` and `opacity`, or their cost was measured and accepted.
|
||||||
- No hardcoded/palette colors were introduced.
|
- No hardcoded/palette colors were introduced.
|
||||||
- Buttons use shared variants and sizes.
|
- Buttons use shared variants and sizes.
|
||||||
|
- Menus and pickers satisfy the keyboard navigation contract without caller-specific key handling for standard shared components.
|
||||||
- Icons use `Icon`/`IconName`, and generated sprite changes are intentional.
|
- Icons use `Icon`/`IconName`, and generated sprite changes are intentional.
|
||||||
- Hover, selection, primary, and status semantics are distinct.
|
- Hover, selection, primary, and status semantics are distinct.
|
||||||
- Light/dark/high-contrast and long-text states remain legible.
|
- Light/dark/high-contrast and long-text states remain legible.
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { parseAgentMentions } from '@/lib/messages/agentMentions';
|
|||||||
import { CONTEXT_METADATA_KEY, draftFromContextPayload } from '@/lib/messages/contextParts';
|
import { CONTEXT_METADATA_KEY, draftFromContextPayload } from '@/lib/messages/contextParts';
|
||||||
import { ComposerStatusBar } from './ComposerStatusBar';
|
import { ComposerStatusBar } from './ComposerStatusBar';
|
||||||
import { shouldSubmitEnter } from './composer/keyboardPolicy';
|
import { shouldSubmitEnter } from './composer/keyboardPolicy';
|
||||||
|
import { getDropdownNavigationKey } from '@/components/ui/dropdown-navigation';
|
||||||
import { PendingChangesBar } from './PendingChangesBar';
|
import { PendingChangesBar } from './PendingChangesBar';
|
||||||
import { useChatColumnSession } from './chatColumnSession';
|
import { useChatColumnSession } from './chatColumnSession';
|
||||||
import { useChatSurfaceMode } from './useChatSurfaceMode';
|
import { useChatSurfaceMode } from './useChatSurfaceMode';
|
||||||
@@ -1873,40 +1874,17 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (openAutocomplete === 'command' && commandRef.current) {
|
const autocomplete = openAutocomplete === 'command' ? commandRef.current
|
||||||
if (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Escape' || e.key === 'Tab') {
|
: openAutocomplete === 'skill' ? skillRef.current
|
||||||
e.preventDefault();
|
: openAutocomplete === 'snippet' ? snippetRef.current
|
||||||
e.stopPropagation();
|
: openAutocomplete === 'mention' ? mentionRef.current
|
||||||
commandRef.current.handleKeyDown(e.key);
|
: null;
|
||||||
return;
|
const autocompleteKey = getDropdownNavigationKey(e) ?? e.key;
|
||||||
}
|
if (autocomplete && (autocompleteKey === 'Enter' || autocompleteKey === 'ArrowUp' || autocompleteKey === 'ArrowDown' || autocompleteKey === 'Escape' || autocompleteKey === 'Tab')) {
|
||||||
}
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
if (openAutocomplete === 'skill' && skillRef.current) {
|
autocomplete.handleKeyDown(autocompleteKey);
|
||||||
if (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Escape' || e.key === 'Tab') {
|
return;
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
skillRef.current.handleKeyDown(e.key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openAutocomplete === 'snippet' && snippetRef.current) {
|
|
||||||
if (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Escape' || e.key === 'Tab') {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
snippetRef.current.handleKeyDown(e.key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openAutocomplete === 'mention' && mentionRef.current) {
|
|
||||||
if (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Escape' || e.key === 'Tab') {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
mentionRef.current.handleKeyDown(e.key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDesktopExpanded && e.key === 'Escape') {
|
if (isDesktopExpanded && e.key === 'Escape') {
|
||||||
|
|||||||
@@ -200,9 +200,13 @@ and the send path reading the same grammar.
|
|||||||
result as transient local state that resets on every close, and commits
|
result as transient local state that resets on every close, and commits
|
||||||
through the existing project-change flow only on explicit activation.
|
through the existing project-change flow only on explicit activation.
|
||||||
Filtering changes the result area below the anchored input without moving
|
Filtering changes the result area below the anchored input without moving
|
||||||
the search field. The
|
the search field. The worktree picker remains a Select; mobile keeps its
|
||||||
worktree Select and the mobile bottom sheets are unchanged. The selectors only
|
bottom sheets. The selectors only consume their shared prefix while the
|
||||||
consume their shared prefix while the draft target UI is mounted.
|
draft target UI is mounted.
|
||||||
|
Keyboard selection returns focus to the current form's composer, including
|
||||||
|
when the selected value is unchanged.
|
||||||
|
- `ChatInput.tsx` maps Ctrl+N/P to the active command, skill, snippet, or
|
||||||
|
mention picker after its IME guard.
|
||||||
|
|
||||||
## Input recall ownership
|
## Input recall ownership
|
||||||
|
|
||||||
@@ -259,10 +263,13 @@ suites that install module mocks are order-dependent.
|
|||||||
|
|
||||||
## Enter preference
|
## Enter preference
|
||||||
|
|
||||||
`keyboardPolicy.ts` owns the submission decision. Until the Chat setting is
|
`keyboardPolicy.ts` owns the submission decision. The expanded desktop composer
|
||||||
changed, desktop Enter sends, mobile and focus mode require Ctrl/Cmd+Enter,
|
always inserts a newline with Enter, including Shift+Enter, and sends with
|
||||||
and Shift-modified Enter does not send. An explicit choice applies across
|
Ctrl/Cmd+Enter; it ignores the Enter-to-send preference. Outside expanded mode,
|
||||||
shared composers; Ctrl/Cmd+Enter sends in either configured mode.
|
until the Chat setting is changed, desktop Enter sends, mobile requires
|
||||||
|
Ctrl/Cmd+Enter, and Shift-modified Enter does not send. An explicit choice
|
||||||
|
applies across the other shared composers; Ctrl/Cmd+Enter sends in either
|
||||||
|
configured mode.
|
||||||
|
|
||||||
CodeMirror's deferred mobile Enter loses modifier information. Untouched
|
CodeMirror's deferred mobile Enter loses modifier information. Untouched
|
||||||
settings restore Shift to keep the original policy. Once configured, with mobile
|
settings restore Shift to keep the original policy. Once configured, with mobile
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ const enterPolicyCases: Array<[string, Partial<EnterKeyPolicyInput>, boolean]> =
|
|||||||
['configured enabled Shift+Enter inserts a newline', { enterToSendConfigured: true, enterToSend: true, shiftKey: true }, false],
|
['configured enabled Shift+Enter inserts a newline', { enterToSendConfigured: true, enterToSend: true, shiftKey: true }, false],
|
||||||
['configured disabled Enter inserts a newline', { enterToSendConfigured: true, enterToSend: false }, false],
|
['configured disabled Enter inserts a newline', { enterToSendConfigured: true, enterToSend: false }, false],
|
||||||
['configured disabled Shift+Enter sends', { enterToSendConfigured: true, enterToSend: false, shiftKey: true }, true],
|
['configured disabled Shift+Enter sends', { enterToSendConfigured: true, enterToSend: false, shiftKey: true }, true],
|
||||||
|
['expanded composer Enter inserts a newline when Enter-to-send is enabled', { isDesktopExpanded: true, enterToSendConfigured: true, enterToSend: true }, false],
|
||||||
|
['expanded composer Shift+Enter inserts a newline when Enter-to-send is disabled', { isDesktopExpanded: true, enterToSendConfigured: true, enterToSend: false, shiftKey: true }, false],
|
||||||
|
['expanded composer Ctrl+Enter sends despite Enter-to-send being disabled', { isDesktopExpanded: true, enterToSendConfigured: true, ctrlKey: true }, true],
|
||||||
|
['expanded composer Cmd+Enter sends despite Enter-to-send being enabled', { isDesktopExpanded: true, enterToSendConfigured: true, enterToSend: true, metaKey: true }, true],
|
||||||
['configured Ctrl+Enter always sends', { enterToSendConfigured: true, isMobile: true, isDesktopExpanded: true, shiftKey: true, ctrlKey: true }, true],
|
['configured Ctrl+Enter always sends', { enterToSendConfigured: true, isMobile: true, isDesktopExpanded: true, shiftKey: true, ctrlKey: true }, true],
|
||||||
['configured Meta+Enter always sends', { enterToSendConfigured: true, isMobile: true, isDesktopExpanded: true, shiftKey: true, metaKey: true }, true],
|
['configured Meta+Enter always sends', { enterToSendConfigured: true, isMobile: true, isDesktopExpanded: true, shiftKey: true, metaKey: true }, true],
|
||||||
];
|
];
|
||||||
@@ -33,8 +37,9 @@ const enterPolicyCases: Array<[string, Partial<EnterKeyPolicyInput>, boolean]> =
|
|||||||
describe('Enter key policy', () => {
|
describe('Enter key policy', () => {
|
||||||
for (const surface of [{}, { isMobile: true }, { isDesktopExpanded: true }]) {
|
for (const surface of [{}, { isMobile: true }, { isDesktopExpanded: true }]) {
|
||||||
for (const modifiers of [{}, { ctrlKey: true }, { metaKey: true }, { ctrlKey: true, metaKey: true }]) {
|
for (const modifiers of [{}, { ctrlKey: true }, { metaKey: true }, { ctrlKey: true, metaKey: true }]) {
|
||||||
test(`untouched Shift+Enter does not submit: ${JSON.stringify({ ...surface, ...modifiers })}`, () => {
|
test(`untouched Shift+Enter only submits with a modifier in expanded mode: ${JSON.stringify({ ...surface, ...modifiers })}`, () => {
|
||||||
expect(shouldSubmitEnter(policy({ ...surface, ...modifiers, shiftKey: true }))).toBe(false);
|
expect(shouldSubmitEnter(policy({ ...surface, ...modifiers, shiftKey: true })))
|
||||||
|
.toBe(Boolean(surface.isDesktopExpanded && (modifiers.ctrlKey || modifiers.metaKey)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ export interface EnterKeyPolicyInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const shouldSubmitEnter = (input: EnterKeyPolicyInput): boolean => {
|
export const shouldSubmitEnter = (input: EnterKeyPolicyInput): boolean => {
|
||||||
const enterSendsByDefault = !input.isMobile && !input.isDesktopExpanded;
|
|
||||||
const isCtrlEnter = input.ctrlKey || input.metaKey;
|
const isCtrlEnter = input.ctrlKey || input.metaKey;
|
||||||
|
if (input.isDesktopExpanded) return isCtrlEnter;
|
||||||
|
|
||||||
|
const enterSendsByDefault = !input.isMobile;
|
||||||
if (!input.enterToSendConfigured) {
|
if (!input.enterToSendConfigured) {
|
||||||
return !input.shiftKey && (enterSendsByDefault || isCtrlEnter);
|
return !input.shiftKey && (enterSendsByDefault || isCtrlEnter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,6 +163,10 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
|
|||||||
const [projectFocusReturn, setProjectFocusReturn] = React.useState(false);
|
const [projectFocusReturn, setProjectFocusReturn] = React.useState(false);
|
||||||
const projectTriggerRef = React.useRef<HTMLButtonElement>(null);
|
const projectTriggerRef = React.useRef<HTMLButtonElement>(null);
|
||||||
const worktreeTriggerRef = React.useRef<HTMLButtonElement>(null);
|
const worktreeTriggerRef = React.useRef<HTMLButtonElement>(null);
|
||||||
|
// Controlled Select closes can omit finalFocus's interaction type.
|
||||||
|
const keyboardCloseRef = React.useRef(false);
|
||||||
|
const getComposerInput = () => projectTriggerRef.current?.closest('form')?.querySelector<HTMLElement>('[data-chat-input="true"] .cm-content');
|
||||||
|
const getFinalFocus = () => keyboardCloseRef.current ? getComposerInput() : true;
|
||||||
const projectSearchRef = React.useRef<HTMLInputElement>(null);
|
const projectSearchRef = React.useRef<HTMLInputElement>(null);
|
||||||
// Preserve Select's dialog portal and main-area containment.
|
// Preserve Select's dialog portal and main-area containment.
|
||||||
const [projectPortalContainer, setProjectPortalContainer] = React.useState<HTMLElement | null>(null);
|
const [projectPortalContainer, setProjectPortalContainer] = React.useState<HTMLElement | null>(null);
|
||||||
@@ -190,6 +194,7 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
|
|||||||
if (openPicker === null || !shouldDismissDropdown(event)) return;
|
if (openPicker === null || !shouldDismissDropdown(event)) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
keyboardCloseRef.current = true;
|
||||||
setOpenPicker(null);
|
setOpenPicker(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -279,10 +284,10 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
|
|||||||
setOpenPicker(null);
|
setOpenPicker(null);
|
||||||
}}
|
}}
|
||||||
onOpenChangeComplete={(open) => {
|
onOpenChangeComplete={(open) => {
|
||||||
// Return focus after Base UI finishes closing so the
|
// Return focus after Base UI finishes closing so typing
|
||||||
// trigger itself, not document body, keeps keyboard flow.
|
// continues in this form's composer, including reselection.
|
||||||
if (!open && projectFocusReturn) {
|
if (!open && projectFocusReturn) {
|
||||||
projectTriggerRef.current?.focus();
|
(getComposerInput() ?? projectTriggerRef.current)?.focus();
|
||||||
setProjectFocusReturn(false);
|
setProjectFocusReturn(false);
|
||||||
}
|
}
|
||||||
// Focus the search once the popup mounts; the opening
|
// Focus the search once the popup mounts; the opening
|
||||||
@@ -403,7 +408,10 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
|
|||||||
<Select
|
<Select
|
||||||
value={selectedDirectory ?? branchItems[0]?.value ?? normalizePath(selectedProject.path) ?? ''}
|
value={selectedDirectory ?? branchItems[0]?.value ?? normalizePath(selectedProject.path) ?? ''}
|
||||||
open={openPicker === 'worktree'}
|
open={openPicker === 'worktree'}
|
||||||
onOpenChange={(open) => setOpenPicker(open ? 'worktree' : null)}
|
onOpenChange={(open, details) => {
|
||||||
|
keyboardCloseRef.current = !open && details.event.type === 'keydown';
|
||||||
|
setOpenPicker(open ? 'worktree' : null);
|
||||||
|
}}
|
||||||
onValueChange={handleDirectoryChange}
|
onValueChange={handleDirectoryChange}
|
||||||
disableGlobalShortcuts
|
disableGlobalShortcuts
|
||||||
>
|
>
|
||||||
@@ -433,7 +441,7 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
) : null}
|
) : null}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<SelectContent side="top" collisionAvoidance={{ side: 'none' }} constrainToMain className="w-max min-w-48" onKeyDown={handlePickerKeyDown}>
|
<SelectContent side="top" collisionAvoidance={{ side: 'none' }} constrainToMain className="w-max min-w-48" onKeyDown={handlePickerKeyDown} finalFocus={getFinalFocus}>
|
||||||
{projectRootBranchOption ? (
|
{projectRootBranchOption ? (
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>{t('chat.chatInput.projectRoot')}</SelectLabel>
|
<SelectLabel>{t('chat.chatInput.projectRoot')}</SelectLabel>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { matchesRankQuery } from '@/lib/search/fuzzySearch';
|
|||||||
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { handleDropdownNavigationKey } from '@/components/ui/dropdown-navigation';
|
||||||
import { getCurrentIntlLocale } from '@/lib/i18n';
|
import { getCurrentIntlLocale } from '@/lib/i18n';
|
||||||
import { mergeModelMetadataWithLiveModel } from '@/lib/modelMetadata';
|
import { mergeModelMetadataWithLiveModel } from '@/lib/modelMetadata';
|
||||||
import { getModelDisplayName as getSharedModelDisplayName } from '@/lib/modelDisplay';
|
import { getModelDisplayName as getSharedModelDisplayName } from '@/lib/modelDisplay';
|
||||||
@@ -585,9 +586,17 @@ export const ModelPickerList: React.FC<ModelPickerListProps> = ({
|
|||||||
filteredFavorites.map((entry) => [`${entry.providerID}:${entry.modelID}`, entry] as const),
|
filteredFavorites.map((entry) => [`${entry.providerID}:${entry.modelID}`, entry] as const),
|
||||||
), [filteredFavorites]);
|
), [filteredFavorites]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
const initialSelectionIndex = searchQuery.trim() || !selectedModel ? 0 : Math.max(0,
|
||||||
selectionStore.set(0);
|
flatModelList.findIndex((entry) => entry.providerID === selectedModel.providerID && entry.modelID === selectedModel.modelID),
|
||||||
}, [searchQuery, selectionStore]);
|
);
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
selectionStore.set(initialSelectionIndex);
|
||||||
|
// Opening or scrolling the list must not let a stationary pointer replace the current model.
|
||||||
|
keyboardOwnsSelectionRef.current = true;
|
||||||
|
lastMousePositionRef.current = null;
|
||||||
|
scrollIntoView(scrollRef.current, itemRefs.current[initialSelectionIndex]);
|
||||||
|
}, [initialSelectionIndex, searchQuery, selectedModel?.providerID, selectedModel?.modelID, selectionStore]);
|
||||||
|
|
||||||
const selectIndex = React.useCallback((index: number) => {
|
const selectIndex = React.useCallback((index: number) => {
|
||||||
selectionStore.set(index);
|
selectionStore.set(index);
|
||||||
@@ -608,10 +617,13 @@ export const ModelPickerList: React.FC<ModelPickerListProps> = ({
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
onActiveEntryChange?.(flatModelList[selectionStore.getSnapshot()]);
|
onActiveEntryChange?.(flatModelList[selectionStore.getSnapshot()]);
|
||||||
}, [flatModelList, onActiveEntryChange, selectionStore]);
|
}, [flatModelList, initialSelectionIndex, onActiveEntryChange, selectionStore]);
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {
|
const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {
|
||||||
if (event.defaultPrevented) return;
|
if (event.defaultPrevented) return;
|
||||||
|
if (handleDropdownNavigationKey(event, (navigationKey) => {
|
||||||
|
moveSelection(navigationKey === 'ArrowDown' ? 1 : -1);
|
||||||
|
})) return;
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if ((event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
|
if ((event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
|
||||||
const selected = flatModelList[selectionStore.getSnapshot()];
|
const selected = flatModelList[selectionStore.getSnapshot()];
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
const setEnterToSend = useUIStore(state => state.setEnterToSend);
|
const setEnterToSend = useUIStore(state => state.setEnterToSend);
|
||||||
const enterToSendConfigured = useUIStore(state => state.enterToSendConfigured);
|
const enterToSendConfigured = useUIStore(state => state.enterToSendConfigured);
|
||||||
const setEnterToSendConfigured = useUIStore(state => state.setEnterToSendConfigured);
|
const setEnterToSendConfigured = useUIStore(state => state.setEnterToSendConfigured);
|
||||||
const isExpandedInput = useUIStore(state => state.isExpandedInput);
|
const enterSendSelected = enterToSendConfigured ? enterToSend : !isMobile;
|
||||||
const showToolFileIcons = useUIStore(state => state.showToolFileIcons);
|
const showToolFileIcons = useUIStore(state => state.showToolFileIcons);
|
||||||
const setShowToolFileIcons = useUIStore(state => state.setShowToolFileIcons);
|
const setShowToolFileIcons = useUIStore(state => state.setShowToolFileIcons);
|
||||||
const showTurnChangedFiles = useUIStore(state => state.showTurnChangedFiles);
|
const showTurnChangedFiles = useUIStore(state => state.showTurnChangedFiles);
|
||||||
@@ -2097,8 +2097,10 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
<SettingsSection
|
<SettingsSection
|
||||||
title={t('settings.openchamber.visual.section.composer')}
|
title={t('settings.openchamber.visual.section.composer')}
|
||||||
settingsItem="chat.composer"
|
settingsItem="chat.composer"
|
||||||
contentClassName={SETTINGS_OPTION_STACK_CLASS}
|
contentClassName="space-y-6"
|
||||||
>
|
>
|
||||||
|
{(shouldShow('persistDraft') || (!isMobile && shouldShow('inputSpellcheck'))) && (
|
||||||
|
<div className={SETTINGS_OPTION_STACK_CLASS}>
|
||||||
{shouldShow('persistDraft') && (
|
{shouldShow('persistDraft') && (
|
||||||
<SettingsCheckboxRow
|
<SettingsCheckboxRow
|
||||||
checked={persistChatDraft}
|
checked={persistChatDraft}
|
||||||
@@ -2118,11 +2120,13 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
settingsItem="chat.spellcheck"
|
settingsItem="chat.spellcheck"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{shouldShow('largeTextPaste') && (
|
{shouldShow('largeTextPaste') && (
|
||||||
<SettingsControlGroup
|
<SettingsControlGroup
|
||||||
title={t('settings.openchamber.visual.field.largeTextPaste')}
|
title={t('settings.openchamber.visual.field.largeTextPaste')}
|
||||||
info={t('settings.openchamber.visual.field.largeTextPasteHint')}
|
description={t('settings.openchamber.visual.field.largeTextPasteHint')}
|
||||||
settingsItem="chat.large-text-paste"
|
settingsItem="chat.large-text-paste"
|
||||||
>
|
>
|
||||||
<SettingsRadioGroup aria-label={t('settings.openchamber.visual.field.largeTextPasteAria')}>
|
<SettingsRadioGroup aria-label={t('settings.openchamber.visual.field.largeTextPasteAria')}>
|
||||||
@@ -2139,14 +2143,26 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
</SettingsControlGroup>
|
</SettingsControlGroup>
|
||||||
)}
|
)}
|
||||||
{shouldShow('enterToSend') && (
|
{shouldShow('enterToSend') && (
|
||||||
<SettingsCheckboxRow
|
<SettingsControlGroup
|
||||||
checked={enterToSendConfigured ? enterToSend : !isMobile && !isExpandedInput}
|
title={t('settings.openchamber.visual.field.enterToSend')}
|
||||||
onChange={handleEnterToSendChange}
|
description={t('settings.openchamber.visual.field.enterToSendHint')}
|
||||||
label={t('settings.openchamber.visual.field.enterToSend')}
|
|
||||||
info={t('settings.openchamber.visual.field.enterToSendHint')}
|
|
||||||
ariaLabel={t('settings.openchamber.visual.field.enterToSend')}
|
|
||||||
settingsItem="chat.enter-to-send"
|
settingsItem="chat.enter-to-send"
|
||||||
/>
|
>
|
||||||
|
<SettingsRadioGroup aria-label={t('settings.openchamber.visual.field.enterToSend')}>
|
||||||
|
<SettingsRadioOption
|
||||||
|
selected={enterSendSelected}
|
||||||
|
onSelect={() => handleEnterToSendChange(true)}
|
||||||
|
label={t('settings.openchamber.visual.option.enterToSend.enter.label')}
|
||||||
|
ariaLabel={t('settings.openchamber.visual.option.enterToSend.enter.label')}
|
||||||
|
/>
|
||||||
|
<SettingsRadioOption
|
||||||
|
selected={!enterSendSelected}
|
||||||
|
onSelect={() => handleEnterToSendChange(false)}
|
||||||
|
label={t('settings.openchamber.visual.option.enterToSend.modifier.label')}
|
||||||
|
ariaLabel={t('settings.openchamber.visual.option.enterToSend.modifier.label')}
|
||||||
|
/>
|
||||||
|
</SettingsRadioGroup>
|
||||||
|
</SettingsControlGroup>
|
||||||
)}
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
dropdownMenuPopupClass,
|
dropdownMenuPopupClass,
|
||||||
dropdownMenuSeparatorClass,
|
dropdownMenuSeparatorClass,
|
||||||
} from "./dropdown-menu.styles";
|
} from "./dropdown-menu.styles";
|
||||||
|
import { handleDropdownNavigationKey } from "./dropdown-navigation";
|
||||||
|
|
||||||
function ContextMenu({ ...props }: React.ComponentProps<typeof BaseContextMenu.Root>) {
|
function ContextMenu({ ...props }: React.ComponentProps<typeof BaseContextMenu.Root>) {
|
||||||
return <BaseContextMenu.Root {...props} />;
|
return <BaseContextMenu.Root {...props} />;
|
||||||
@@ -22,7 +23,18 @@ type ContentProps = {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
} & React.ComponentProps<typeof BaseContextMenu.Popup>;
|
} & React.ComponentProps<typeof BaseContextMenu.Popup>;
|
||||||
|
|
||||||
function ContextMenuContent({ className, positionerClassName, children, style, ...props }: ContentProps) {
|
function ContextMenuContent({ className, positionerClassName, children, style, onKeyDown, ...props }: ContentProps) {
|
||||||
|
const handleKeyDown: NonNullable<React.ComponentProps<typeof BaseContextMenu.Popup>["onKeyDown"]> = (event) => {
|
||||||
|
onKeyDown?.(event);
|
||||||
|
handleDropdownNavigationKey(event, (navigationKey) => {
|
||||||
|
event.currentTarget.dispatchEvent(new KeyboardEvent("keydown", {
|
||||||
|
key: navigationKey,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseContextMenu.Portal>
|
<BaseContextMenu.Portal>
|
||||||
<BaseContextMenu.Positioner className={cn("app-region-no-drag z-50", positionerClassName)}>
|
<BaseContextMenu.Positioner className={cn("app-region-no-drag z-50", positionerClassName)}>
|
||||||
@@ -34,6 +46,7 @@ function ContextMenuContent({ className, positionerClassName, children, style, .
|
|||||||
}}
|
}}
|
||||||
className={cn(dropdownMenuPopupClass, className)}
|
className={cn(dropdownMenuPopupClass, className)}
|
||||||
{...props}
|
{...props}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</BaseContextMenu.Popup>
|
</BaseContextMenu.Popup>
|
||||||
|
|||||||
@@ -322,9 +322,21 @@ function DropdownMenuSubTrigger({
|
|||||||
function DropdownMenuSubContent({
|
function DropdownMenuSubContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
onKeyDown,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof BaseMenu.Popup>) {
|
}: React.ComponentProps<typeof BaseMenu.Popup>) {
|
||||||
const portalContext = React.useContext(DropdownPortalContext);
|
const portalContext = React.useContext(DropdownPortalContext);
|
||||||
|
const handleKeyDown: NonNullable<React.ComponentProps<typeof BaseMenu.Popup>['onKeyDown']> = (event) => {
|
||||||
|
onKeyDown?.(event);
|
||||||
|
handleDropdownNavigationKey(event, (navigationKey) => {
|
||||||
|
event.currentTarget.dispatchEvent(new KeyboardEvent('keydown', {
|
||||||
|
key: navigationKey,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseMenu.Portal container={portalContext?.portalContainer || undefined}>
|
<BaseMenu.Portal container={portalContext?.portalContainer || undefined}>
|
||||||
<BaseMenu.Positioner className="z-50">
|
<BaseMenu.Positioner className="z-50">
|
||||||
@@ -338,6 +350,7 @@ function DropdownMenuSubContent({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</BaseMenu.Popup>
|
</BaseMenu.Popup>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
|
import { getDropdownNavigationKey, handleDropdownNavigationKey } from './dropdown-navigation';
|
||||||
|
|
||||||
|
function createEvent(overrides: Partial<KeyboardEvent> = {}) {
|
||||||
|
let defaultPrevented = false;
|
||||||
|
let propagationStopped = false;
|
||||||
|
return {
|
||||||
|
key: 'n',
|
||||||
|
code: 'KeyN',
|
||||||
|
ctrlKey: true,
|
||||||
|
metaKey: false,
|
||||||
|
altKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
get defaultPrevented() { return defaultPrevented; },
|
||||||
|
isPropagationStopped: () => propagationStopped,
|
||||||
|
preventDefault: () => { defaultPrevented = true; },
|
||||||
|
stopPropagation: () => { propagationStopped = true; },
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('dropdown Ctrl+N/P navigation', () => {
|
||||||
|
test('requires an exact Ctrl modifier chord', () => {
|
||||||
|
expect(getDropdownNavigationKey(createEvent())).toBe('ArrowDown');
|
||||||
|
expect(getDropdownNavigationKey(createEvent({ key: 'p', code: 'KeyP' }))).toBe('ArrowUp');
|
||||||
|
expect(getDropdownNavigationKey(createEvent({ ctrlKey: false }))).toBeNull();
|
||||||
|
expect(getDropdownNavigationKey(createEvent({ metaKey: true }))).toBeNull();
|
||||||
|
expect(getDropdownNavigationKey(createEvent({ altKey: true }))).toBeNull();
|
||||||
|
expect(getDropdownNavigationKey(createEvent({ shiftKey: true }))).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses the physical key for non-Latin layouts', () => {
|
||||||
|
expect(getDropdownNavigationKey(createEvent({ key: 'т', code: 'KeyN' }))).toBe('ArrowDown');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('respects cancellation and otherwise navigates exactly once', () => {
|
||||||
|
const cancelled = createEvent({ defaultPrevented: true });
|
||||||
|
expect(handleDropdownNavigationKey(cancelled, () => { throw new Error('should not navigate'); })).toBe(false);
|
||||||
|
|
||||||
|
const stopped = createEvent();
|
||||||
|
stopped.stopPropagation();
|
||||||
|
expect(handleDropdownNavigationKey(stopped, () => { throw new Error('should not navigate'); })).toBe(false);
|
||||||
|
|
||||||
|
const event = createEvent();
|
||||||
|
const steps: string[] = [];
|
||||||
|
expect(handleDropdownNavigationKey(event, (key) => steps.push(key))).toBe(true);
|
||||||
|
expect(steps).toEqual(['ArrowDown']);
|
||||||
|
expect(event.defaultPrevented).toBe(true);
|
||||||
|
expect(event.isPropagationStopped()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@ import type React from 'react';
|
|||||||
|
|
||||||
import { isIMECompositionEvent } from '@/lib/ime';
|
import { isIMECompositionEvent } from '@/lib/ime';
|
||||||
|
|
||||||
function getDropdownNavigationKey(event: Pick<KeyboardEvent, 'key' | 'code' | 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>): 'ArrowDown' | 'ArrowUp' | null {
|
export function getDropdownNavigationKey(event: Pick<KeyboardEvent, 'key' | 'code' | 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>): 'ArrowDown' | 'ArrowUp' | null {
|
||||||
if (!event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return null;
|
if (!event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return null;
|
||||||
// `code` covers non-Latin layouts, where `key` is the layout's own letter.
|
// `code` covers non-Latin layouts, where `key` is the layout's own letter.
|
||||||
if (event.key.toLowerCase() === 'n' || event.code === 'KeyN') return 'ArrowDown';
|
if (event.key.toLowerCase() === 'n' || event.code === 'KeyN') return 'ArrowDown';
|
||||||
|
|||||||
@@ -2042,8 +2042,10 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.enableSpellcheckInTextInputs': 'Rechtschreibprüfung in Texteingaben aktivieren',
|
'settings.openchamber.visual.field.enableSpellcheckInTextInputs': 'Rechtschreibprüfung in Texteingaben aktivieren',
|
||||||
'settings.openchamber.visual.field.largeTextPaste': 'Großes Texteinfügen',
|
'settings.openchamber.visual.field.largeTextPaste': 'Großes Texteinfügen',
|
||||||
'settings.openchamber.visual.field.largeTextPasteHint': 'Beim Einfügen von mehr als etwa 2.000 Zeichen oder 25 Zeilen wählen, ob der Text als Datei angehängt, direkt eingefügt oder jedes Mal nachgefragt werden soll.',
|
'settings.openchamber.visual.field.largeTextPasteHint': 'Beim Einfügen von mehr als etwa 2.000 Zeichen oder 25 Zeilen wählen, ob der Text als Datei angehängt, direkt eingefügt oder jedes Mal nachgefragt werden soll.',
|
||||||
'settings.openchamber.visual.field.enterToSend': 'Enter sendet',
|
'settings.openchamber.visual.field.enterToSend': 'Tastenkürzel zum Senden',
|
||||||
'settings.openchamber.visual.field.enterToSendHint': 'Nach der Änderung steuern Enter und Shift+Enter das Verhalten auf jeder Oberfläche. Bis dahin behält jede Oberfläche ihr bestehendes Verhalten bei.',
|
'settings.openchamber.visual.field.enterToSendHint': 'Wählen Sie das Tastenkürzel zum Senden im Standard-Composer. Im erweiterten Composer fügt Enter immer eine neue Zeile ein, und Strg/Cmd+Enter sendet.',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.enter.label': 'Mit Enter senden',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.modifier.label': 'Mit Strg/Cmd+Enter senden',
|
||||||
'settings.openchamber.visual.field.largeTextPasteAria': 'Verhalten bei großem Texteinfügen',
|
'settings.openchamber.visual.field.largeTextPasteAria': 'Verhalten bei großem Texteinfügen',
|
||||||
'settings.openchamber.visual.field.largeTextPasteOptionAria': 'Großes Texteinfügen: {option}',
|
'settings.openchamber.visual.field.largeTextPasteOptionAria': 'Großes Texteinfügen: {option}',
|
||||||
'settings.openchamber.visual.option.largeTextPaste.ask.label': 'Jedes Mal fragen',
|
'settings.openchamber.visual.option.largeTextPaste.ask.label': 'Jedes Mal fragen',
|
||||||
|
|||||||
@@ -2125,8 +2125,10 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.enableSpellcheckInTextInputs': 'Enable Spellcheck in Text Inputs',
|
'settings.openchamber.visual.field.enableSpellcheckInTextInputs': 'Enable Spellcheck in Text Inputs',
|
||||||
'settings.openchamber.visual.field.largeTextPaste': 'Large text paste',
|
'settings.openchamber.visual.field.largeTextPaste': 'Large text paste',
|
||||||
'settings.openchamber.visual.field.largeTextPasteHint': 'When pasting more than about 2,000 characters or 25 lines, choose whether to attach the text as a file, paste it inline, or ask each time.',
|
'settings.openchamber.visual.field.largeTextPasteHint': 'When pasting more than about 2,000 characters or 25 lines, choose whether to attach the text as a file, paste it inline, or ask each time.',
|
||||||
'settings.openchamber.visual.field.enterToSend': 'Enter sends',
|
'settings.openchamber.visual.field.enterToSend': 'Send shortcut',
|
||||||
'settings.openchamber.visual.field.enterToSendHint': 'Once changed, this controls Enter and Shift+Enter on every surface. Until then, each surface keeps its existing behavior.',
|
'settings.openchamber.visual.field.enterToSendHint': 'Choose the send shortcut for the standard composer. In the expanded composer, Enter always adds a new line and Ctrl/Cmd+Enter sends.',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.enter.label': 'Send with Enter',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.modifier.label': 'Send with Ctrl/Cmd+Enter',
|
||||||
'settings.openchamber.visual.field.largeTextPasteAria': 'Large text paste behavior',
|
'settings.openchamber.visual.field.largeTextPasteAria': 'Large text paste behavior',
|
||||||
'settings.openchamber.visual.field.largeTextPasteOptionAria': 'Large text paste: {option}',
|
'settings.openchamber.visual.field.largeTextPasteOptionAria': 'Large text paste: {option}',
|
||||||
'settings.openchamber.visual.option.largeTextPaste.ask.label': 'Ask each time',
|
'settings.openchamber.visual.option.largeTextPaste.ask.label': 'Ask each time',
|
||||||
|
|||||||
@@ -2274,8 +2274,10 @@ export const settingsDict = {
|
|||||||
"settings.openchamber.visual.field.inputHistoryLimitDescription": "Bajar este número elimina de inmediato los prompts más antiguos de tu historial.",
|
"settings.openchamber.visual.field.inputHistoryLimitDescription": "Bajar este número elimina de inmediato los prompts más antiguos de tu historial.",
|
||||||
"settings.openchamber.visual.field.inputHistoryLimitAria": "Prompts que recordar",
|
"settings.openchamber.visual.field.inputHistoryLimitAria": "Prompts que recordar",
|
||||||
"settings.openchamber.visual.field.inputHistoryLimitUnit": "prompts",
|
"settings.openchamber.visual.field.inputHistoryLimitUnit": "prompts",
|
||||||
"settings.openchamber.visual.field.enterToSend": "Enter envía",
|
"settings.openchamber.visual.field.enterToSend": "Atajo de envío",
|
||||||
"settings.openchamber.visual.field.enterToSendHint": "Después de cambiarlo, controla Enter y Shift+Enter en todas las superficies. Hasta entonces, cada superficie mantiene su comportamiento actual.",
|
"settings.openchamber.visual.field.enterToSendHint": "Elige el atajo de envío para el compositor estándar. En el compositor expandido, Intro siempre añade una nueva línea y Ctrl/Cmd+Intro envía.",
|
||||||
|
"settings.openchamber.visual.option.enterToSend.enter.label": "Enviar con Intro",
|
||||||
|
"settings.openchamber.visual.option.enterToSend.modifier.label": "Enviar con Ctrl/Cmd+Intro",
|
||||||
...linearIntegrationI18n.es,
|
...linearIntegrationI18n.es,
|
||||||
'settings.page.integrations.title': 'Integraciones',
|
'settings.page.integrations.title': 'Integraciones',
|
||||||
'settings.page.integrations.description': 'Conecta GitHub y Linear para que OpenChamber pueda trabajar con tus issues y pull requests.',
|
'settings.page.integrations.description': 'Conecta GitHub y Linear para que OpenChamber pueda trabajar con tus issues y pull requests.',
|
||||||
|
|||||||
@@ -2274,8 +2274,10 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'Réduire ce nombre supprime aussitôt les prompts les plus anciens de votre historique.',
|
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'Réduire ce nombre supprime aussitôt les prompts les plus anciens de votre historique.',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitAria': 'Prompts à mémoriser',
|
'settings.openchamber.visual.field.inputHistoryLimitAria': 'Prompts à mémoriser',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitUnit': 'prompts',
|
'settings.openchamber.visual.field.inputHistoryLimitUnit': 'prompts',
|
||||||
'settings.openchamber.visual.field.enterToSend': 'Entrée envoie',
|
'settings.openchamber.visual.field.enterToSend': 'Raccourci d\'envoi',
|
||||||
'settings.openchamber.visual.field.enterToSendHint': 'Après modification, ce réglage contrôle Entrée et Maj+Entrée sur toutes les surfaces. En attendant, chaque surface conserve son comportement actuel.',
|
'settings.openchamber.visual.field.enterToSendHint': 'Choisissez le raccourci d\'envoi pour le composeur standard. Dans le composeur étendu, Entrée ajoute toujours une nouvelle ligne et Ctrl/Cmd+Entrée envoie.',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.enter.label': 'Envoyer avec Entrée',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.modifier.label': 'Envoyer avec Ctrl/Cmd+Entrée',
|
||||||
...linearIntegrationI18n.fr,
|
...linearIntegrationI18n.fr,
|
||||||
'settings.page.integrations.title': 'Intégrations',
|
'settings.page.integrations.title': 'Intégrations',
|
||||||
'settings.page.integrations.description': 'Connectez GitHub et Linear pour qu’OpenChamber puisse travailler avec vos issues et pull requests.',
|
'settings.page.integrations.description': 'Connectez GitHub et Linear pour qu’OpenChamber puisse travailler avec vos issues et pull requests.',
|
||||||
|
|||||||
@@ -2274,8 +2274,10 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'この数を減らすと、履歴内の古いプロンプトはすぐに削除されます。',
|
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'この数を減らすと、履歴内の古いプロンプトはすぐに削除されます。',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitAria': '記憶するプロンプト数',
|
'settings.openchamber.visual.field.inputHistoryLimitAria': '記憶するプロンプト数',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitUnit': '件',
|
'settings.openchamber.visual.field.inputHistoryLimitUnit': '件',
|
||||||
'settings.openchamber.visual.field.enterToSend': 'Enterで送信',
|
'settings.openchamber.visual.field.enterToSend': '送信ショートカット',
|
||||||
'settings.openchamber.visual.field.enterToSendHint': '変更すると、すべての環境でEnterとShift+Enterの動作を制御します。変更するまでは、各環境の既存の動作が維持されます。',
|
'settings.openchamber.visual.field.enterToSendHint': '標準コンポーザーの送信ショートカットを選択します。拡張コンポーザーでは、Enter は常に改行し、Ctrl/Cmd+Enter で送信します。',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.enter.label': 'Enter で送信',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.modifier.label': 'Ctrl/Cmd+Enter で送信',
|
||||||
...linearIntegrationI18n.ja,
|
...linearIntegrationI18n.ja,
|
||||||
'settings.page.integrations.title': '連携',
|
'settings.page.integrations.title': '連携',
|
||||||
'settings.page.integrations.description': 'GitHub と Linear を接続すると、OpenChamber が Issue やプルリクエストを扱えるようになります。',
|
'settings.page.integrations.description': 'GitHub と Linear を接続すると、OpenChamber が Issue やプルリクエストを扱えるようになります。',
|
||||||
|
|||||||
@@ -2274,8 +2274,10 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.inputHistoryLimitDescription': '이 숫자를 낮추면 기록에서 오래된 프롬프트가 바로 삭제됩니다.',
|
'settings.openchamber.visual.field.inputHistoryLimitDescription': '이 숫자를 낮추면 기록에서 오래된 프롬프트가 바로 삭제됩니다.',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitAria': '기억할 프롬프트 수',
|
'settings.openchamber.visual.field.inputHistoryLimitAria': '기억할 프롬프트 수',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitUnit': '개',
|
'settings.openchamber.visual.field.inputHistoryLimitUnit': '개',
|
||||||
'settings.openchamber.visual.field.enterToSend': 'Enter로 전송',
|
'settings.openchamber.visual.field.enterToSend': '전송 단축키',
|
||||||
'settings.openchamber.visual.field.enterToSendHint': '변경하면 모든 환경에서 Enter와 Shift+Enter의 동작을 제어합니다. 변경하기 전에는 각 환경의 기존 동작이 유지됩니다.',
|
'settings.openchamber.visual.field.enterToSendHint': '일반 작성기의 전송 단축키를 선택하세요. 확장 작성기에서는 Enter가 항상 줄바꿈을 하고 Ctrl/Cmd+Enter로 전송합니다.',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.enter.label': 'Enter로 전송',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.modifier.label': 'Ctrl/Cmd+Enter로 전송',
|
||||||
...linearIntegrationI18n.ko,
|
...linearIntegrationI18n.ko,
|
||||||
'settings.page.integrations.title': '통합',
|
'settings.page.integrations.title': '통합',
|
||||||
'settings.page.integrations.description': 'GitHub와 Linear를 연결하면 OpenChamber가 이슈와 풀 리퀘스트를 다룰 수 있습니다.',
|
'settings.page.integrations.description': 'GitHub와 Linear를 연결하면 OpenChamber가 이슈와 풀 리퀘스트를 다룰 수 있습니다.',
|
||||||
|
|||||||
@@ -2267,8 +2267,10 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'Zmniejszenie tej liczby od razu usuwa starsze prompty z historii.',
|
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'Zmniejszenie tej liczby od razu usuwa starsze prompty z historii.',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitAria': 'Liczba zapamiętywanych promptów',
|
'settings.openchamber.visual.field.inputHistoryLimitAria': 'Liczba zapamiętywanych promptów',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitUnit': 'promptów',
|
'settings.openchamber.visual.field.inputHistoryLimitUnit': 'promptów',
|
||||||
'settings.openchamber.visual.field.enterToSend': 'Enter wysyła',
|
'settings.openchamber.visual.field.enterToSend': 'Skrót wysyłania',
|
||||||
'settings.openchamber.visual.field.enterToSendHint': 'Po zmianie ustawienie steruje działaniem klawiszy Enter i Shift+Enter na każdej powierzchni. Do tego czasu każda powierzchnia zachowuje dotychczasowe działanie.',
|
'settings.openchamber.visual.field.enterToSendHint': 'Wybierz skrót wysyłania dla standardowego komponentu. W rozszerzonym komponencie Enter zawsze dodaje nowy wiersz, a Ctrl/Cmd+Enter wysyła.',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.enter.label': 'Wyślij klawiszem Enter',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.modifier.label': 'Wyślij klawiszami Ctrl/Cmd+Enter',
|
||||||
...linearIntegrationI18n.pl,
|
...linearIntegrationI18n.pl,
|
||||||
'settings.page.integrations.title': 'Integracje',
|
'settings.page.integrations.title': 'Integracje',
|
||||||
'settings.page.integrations.description': 'Połącz GitHub i Linear, aby OpenChamber mógł pracować z Twoimi issue i pull requestami.',
|
'settings.page.integrations.description': 'Połącz GitHub i Linear, aby OpenChamber mógł pracować z Twoimi issue i pull requestami.',
|
||||||
|
|||||||
@@ -2274,8 +2274,10 @@ export const settingsDict = {
|
|||||||
"settings.openchamber.visual.field.inputHistoryLimitDescription": "Reduzir esse número remove na hora os prompts mais antigos do seu histórico.",
|
"settings.openchamber.visual.field.inputHistoryLimitDescription": "Reduzir esse número remove na hora os prompts mais antigos do seu histórico.",
|
||||||
"settings.openchamber.visual.field.inputHistoryLimitAria": "Prompts para lembrar",
|
"settings.openchamber.visual.field.inputHistoryLimitAria": "Prompts para lembrar",
|
||||||
"settings.openchamber.visual.field.inputHistoryLimitUnit": "prompts",
|
"settings.openchamber.visual.field.inputHistoryLimitUnit": "prompts",
|
||||||
"settings.openchamber.visual.field.enterToSend": "Enter envia",
|
"settings.openchamber.visual.field.enterToSend": "Atalho de envio",
|
||||||
"settings.openchamber.visual.field.enterToSendHint": "Depois de alterada, esta opção controla Enter e Shift+Enter em todas as superfícies. Até lá, cada superfície mantém seu comportamento atual.",
|
"settings.openchamber.visual.field.enterToSendHint": "Escolha o atalho de envio para o compositor padrão. No compositor expandido, Enter sempre cria uma nova linha e Ctrl/Cmd+Enter envia.",
|
||||||
|
"settings.openchamber.visual.option.enterToSend.enter.label": "Enviar com Enter",
|
||||||
|
"settings.openchamber.visual.option.enterToSend.modifier.label": "Enviar com Ctrl/Cmd+Enter",
|
||||||
...linearIntegrationI18n['pt-BR'],
|
...linearIntegrationI18n['pt-BR'],
|
||||||
'settings.page.integrations.title': 'Integrações',
|
'settings.page.integrations.title': 'Integrações',
|
||||||
'settings.page.integrations.description': 'Conecte o GitHub e o Linear para que o OpenChamber possa trabalhar com suas issues e pull requests.',
|
'settings.page.integrations.description': 'Conecte o GitHub e o Linear para que o OpenChamber possa trabalhar com suas issues e pull requests.',
|
||||||
|
|||||||
@@ -1987,8 +1987,10 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'Bu sayıyı azaltmak, eski prompt\'ları geçmişinizden hemen siler.',
|
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'Bu sayıyı azaltmak, eski prompt\'ları geçmişinizden hemen siler.',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitAria': 'Hatırlanacak prompt sayısı',
|
'settings.openchamber.visual.field.inputHistoryLimitAria': 'Hatırlanacak prompt sayısı',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitUnit': 'prompt',
|
'settings.openchamber.visual.field.inputHistoryLimitUnit': 'prompt',
|
||||||
'settings.openchamber.visual.field.enterToSend': 'Enter gönderir',
|
'settings.openchamber.visual.field.enterToSend': 'Gönderme kısayolu',
|
||||||
'settings.openchamber.visual.field.enterToSendHint': 'Değiştirildikten sonra Enter ve Shift+Enter davranışını tüm yüzeylerde kontrol eder. O zamana kadar her yüzey mevcut davranışını korur.',
|
'settings.openchamber.visual.field.enterToSendHint': 'Standart oluşturucu için gönderme kısayolunu seçin. Genişletilmiş oluşturucuda Enter her zaman yeni satır ekler, Ctrl/Cmd+Enter gönderir.',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.enter.label': 'Enter ile gönder',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.modifier.label': 'Ctrl/Cmd+Enter ile gönder',
|
||||||
'settings.openchamber.visual.actions.resetInputBarOffsetAria': 'Giriş çubuğu ofsetini sıfırla',
|
'settings.openchamber.visual.actions.resetInputBarOffsetAria': 'Giriş çubuğu ofsetini sıfırla',
|
||||||
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Terminal hızlı tuşları',
|
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Terminal hızlı tuşları',
|
||||||
'settings.openchamber.visual.field.terminalQuickKeys': 'Terminal Hızlı Tuşları',
|
'settings.openchamber.visual.field.terminalQuickKeys': 'Terminal Hızlı Tuşları',
|
||||||
|
|||||||
@@ -2274,8 +2274,10 @@ export const settingsDict = {
|
|||||||
"settings.openchamber.visual.field.inputHistoryLimitDescription": "Якщо зменшити це число, старіші промпти одразу буде видалено з історії.",
|
"settings.openchamber.visual.field.inputHistoryLimitDescription": "Якщо зменшити це число, старіші промпти одразу буде видалено з історії.",
|
||||||
"settings.openchamber.visual.field.inputHistoryLimitAria": "Скільки промптів пам’ятати",
|
"settings.openchamber.visual.field.inputHistoryLimitAria": "Скільки промптів пам’ятати",
|
||||||
"settings.openchamber.visual.field.inputHistoryLimitUnit": "промптів",
|
"settings.openchamber.visual.field.inputHistoryLimitUnit": "промптів",
|
||||||
"settings.openchamber.visual.field.enterToSend": "Enter надсилає",
|
"settings.openchamber.visual.field.enterToSend": "Комбінація для надсилання",
|
||||||
"settings.openchamber.visual.field.enterToSendHint": "Після зміни цей параметр керує поведінкою Enter і Shift+Enter на всіх поверхнях. До цього кожна поверхня зберігає свою поточну поведінку.",
|
"settings.openchamber.visual.field.enterToSendHint": "Виберіть комбінацію для надсилання у стандартному композері. У розгорнутому композері Enter завжди додає новий рядок, а Ctrl/Cmd+Enter надсилає.",
|
||||||
|
"settings.openchamber.visual.option.enterToSend.enter.label": "Надсилати клавішею Enter",
|
||||||
|
"settings.openchamber.visual.option.enterToSend.modifier.label": "Надсилати за допомогою Ctrl/Cmd+Enter",
|
||||||
...linearIntegrationI18n.uk,
|
...linearIntegrationI18n.uk,
|
||||||
'settings.page.integrations.title': 'Інтеграції',
|
'settings.page.integrations.title': 'Інтеграції',
|
||||||
'settings.page.integrations.description': 'Підключіть GitHub і Linear, щоб OpenChamber міг працювати з вашими задачами та pull request-ами.',
|
'settings.page.integrations.description': 'Підключіть GitHub і Linear, щоб OpenChamber міг працювати з вашими задачами та pull request-ами.',
|
||||||
|
|||||||
@@ -2274,8 +2274,10 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.inputHistoryLimitDescription': '调低这个数字会立即从历史记录中删除较早的提示词。',
|
'settings.openchamber.visual.field.inputHistoryLimitDescription': '调低这个数字会立即从历史记录中删除较早的提示词。',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitAria': '要记住的提示词数量',
|
'settings.openchamber.visual.field.inputHistoryLimitAria': '要记住的提示词数量',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitUnit': '条',
|
'settings.openchamber.visual.field.inputHistoryLimitUnit': '条',
|
||||||
'settings.openchamber.visual.field.enterToSend': 'Enter 发送',
|
'settings.openchamber.visual.field.enterToSend': '发送快捷键',
|
||||||
'settings.openchamber.visual.field.enterToSendHint': '更改后,此设置会控制所有界面中的 Enter 和 Shift+Enter。更改前,各界面保持现有行为。',
|
'settings.openchamber.visual.field.enterToSendHint': '请选择标准输入框的发送快捷键。在展开的输入框中,Enter 始终换行,Ctrl/Cmd+Enter 发送。',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.enter.label': '按 Enter 发送',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.modifier.label': '按 Ctrl/Cmd+Enter 发送',
|
||||||
...linearIntegrationI18n['zh-CN'],
|
...linearIntegrationI18n['zh-CN'],
|
||||||
'settings.page.integrations.title': '集成',
|
'settings.page.integrations.title': '集成',
|
||||||
'settings.page.integrations.description': '连接 GitHub 和 Linear,让 OpenChamber 可以处理你的 issue 和拉取请求。',
|
'settings.page.integrations.description': '连接 GitHub 和 Linear,让 OpenChamber 可以处理你的 issue 和拉取请求。',
|
||||||
|
|||||||
@@ -2274,8 +2274,10 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.inputHistoryLimitDescription': '調低這個數字會立刻從歷史記錄移除較早的提示詞。',
|
'settings.openchamber.visual.field.inputHistoryLimitDescription': '調低這個數字會立刻從歷史記錄移除較早的提示詞。',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitAria': '要記住的提示詞數量',
|
'settings.openchamber.visual.field.inputHistoryLimitAria': '要記住的提示詞數量',
|
||||||
'settings.openchamber.visual.field.inputHistoryLimitUnit': '則',
|
'settings.openchamber.visual.field.inputHistoryLimitUnit': '則',
|
||||||
'settings.openchamber.visual.field.enterToSend': 'Enter 傳送',
|
'settings.openchamber.visual.field.enterToSend': '傳送快捷鍵',
|
||||||
'settings.openchamber.visual.field.enterToSendHint': '變更後,此設定會控制所有介面中的 Enter 與 Shift+Enter。變更前,各介面會維持現有行為。',
|
'settings.openchamber.visual.field.enterToSendHint': '請選擇標準輸入框的傳送快捷鍵。在展開的輸入框中,Enter 一律換行,Ctrl/Cmd+Enter 傳送。',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.enter.label': '按 Enter 傳送',
|
||||||
|
'settings.openchamber.visual.option.enterToSend.modifier.label': '按 Ctrl/Cmd+Enter 傳送',
|
||||||
...linearIntegrationI18n['zh-TW'],
|
...linearIntegrationI18n['zh-TW'],
|
||||||
'settings.page.integrations.title': '整合',
|
'settings.page.integrations.title': '整合',
|
||||||
'settings.page.integrations.description': '連接 GitHub 和 Linear,讓 OpenChamber 可以處理你的 issue 和 pull request。',
|
'settings.page.integrations.description': '連接 GitHub 和 Linear,讓 OpenChamber 可以處理你的 issue 和 pull request。',
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
|
|||||||
page: 'chat',
|
page: 'chat',
|
||||||
titleKey: 'settings.openchamber.visual.field.enterToSend',
|
titleKey: 'settings.openchamber.visual.field.enterToSend',
|
||||||
descriptionKey: 'settings.openchamber.visual.field.enterToSendHint',
|
descriptionKey: 'settings.openchamber.visual.field.enterToSendHint',
|
||||||
keywords: ['enter', 'shift enter', 'send', 'newline'],
|
keywords: ['enter', 'shift enter', 'ctrl enter', 'cmd enter', 'mod enter', 'send', 'newline'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sessions.default-model',
|
id: 'sessions.default-model',
|
||||||
|
|||||||
Reference in New Issue
Block a user