fix: harden and de-slop the merged sidebar/chat/settings batch
Post-merge follow-ups for #2740 #2735 #2734 #2690 #2676 #2738 #2684 #2689 #2733 #2739 #2462 #2687 #2736 #2618 #2697, plus three regressions found while reviewing them: - ctrl/cmd+digit while typing no longer switches session tabs (#2503 was still open in practice: the guard only covered the mod+alt surface binding) - Shiki template-call sanitizer now covers every bundled grammar, including the js/ts aliases and embedding grammars; timed-out highlight requests are memoized and no longer cancel unrelated in-flight requests - settings flush on suspend uses keepalive and also fires on Capacitor appStateChange; keeps the selected model persisted across mode switches - remote-only branches fetch before checkout; range helpers fail clearly - git status invalidation now fires for runtime adapters too - settings number inputs and select triggers size in ch so they scale with the interface font - recent-activity timestamps tick from one list-level ticker - Markdown preview find goes through the shared find_in_file keybind with containment, no longer counts its own bar, and debounces observer runs - #2676 reverted; #2524 fixed by fading the sticky header's own background instead of overlaying the content below it - sticky group headers in the model picker and sidebar render again (oc-sticky-fade-scroller class restored after9b9d7069c) - project switcher names are left-aligned again (wrapper lost in26dbc2f30) - tool card quick-open icon is always visible and opens the same line as the expanded card's button - tautological tests replaced or removed; new oxlint findings fixed
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
SettingsStackedField,
|
||||
SettingsChipGroup,
|
||||
SETTINGS_SELECT_SIZE,
|
||||
SETTINGS_NUMBER_INPUT_CLASS,
|
||||
SETTINGS_SELECT_ROW_TRIGGER_CLASS,
|
||||
SETTINGS_ICON_BUTTON_CLASS,
|
||||
SETTINGS_CUSTOM_TRIGGER_CLASS,
|
||||
@@ -450,7 +451,7 @@ export const AgentsPage: React.FC = () => {
|
||||
inputMode="decimal"
|
||||
placeholder="—"
|
||||
emptyLabel="—"
|
||||
className="w-20"
|
||||
className={SETTINGS_NUMBER_INPUT_CLASS}
|
||||
/>
|
||||
{temperature !== undefined && (
|
||||
<Button
|
||||
@@ -488,7 +489,7 @@ export const AgentsPage: React.FC = () => {
|
||||
inputMode="decimal"
|
||||
placeholder="—"
|
||||
emptyLabel="—"
|
||||
className="w-20"
|
||||
className={SETTINGS_NUMBER_INPUT_CLASS}
|
||||
/>
|
||||
{topP !== undefined && (
|
||||
<Button
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { toast } from '@/components/ui';
|
||||
import { useI18n, type I18nKey } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reportSettingsSaveState } from '@/lib/persistence';
|
||||
import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs';
|
||||
import {
|
||||
@@ -376,7 +375,7 @@ export const BehaviorPage: React.FC = () => {
|
||||
onValueChange={(value) => setResponseStylePreset(value)}
|
||||
disabled={isLoading || !responseStyleEnabled}
|
||||
>
|
||||
<SelectTrigger size={SETTINGS_SELECT_SIZE} className={cn(SETTINGS_SELECT_ROW_TRIGGER_CLASS, 'max-w-72')}>
|
||||
<SelectTrigger size={SETTINGS_SELECT_SIZE} className={SETTINGS_SELECT_ROW_TRIGGER_CLASS}>
|
||||
<SelectValue>
|
||||
{(value) => {
|
||||
if (value === 'custom') return t('settings.behavior.page.responseStyle.option.custom');
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
SETTINGS_CLUSTER_CONTROL_CLASS,
|
||||
SETTINGS_NUMBER_STEPPER_ROW_CLASS,
|
||||
SETTINGS_NUMBER_UNIT_CLASS,
|
||||
SETTINGS_NUMBER_INPUT_CLASS,
|
||||
SETTINGS_FIELDS_STACK_CLASS,
|
||||
SETTINGS_OPTION_STACK_CLASS,
|
||||
} from '@/components/sections/shared/SettingsSection';
|
||||
@@ -1217,7 +1218,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
controlClassName="w-full"
|
||||
>
|
||||
<Select value={uiFont} onValueChange={(value) => setUiFont(value as UiFontOption)}>
|
||||
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectInterfaceFontAria')} size={SETTINGS_SELECT_SIZE} className={cn(SETTINGS_SELECT_TRIGGER_CLASS, 'max-w-full')}>
|
||||
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectInterfaceFontAria')} size={SETTINGS_SELECT_SIZE} className={SETTINGS_SELECT_TRIGGER_CLASS}>
|
||||
<SelectValue>{UI_FONT_OPTIONS.find((option) => option.id === uiFont)?.label}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -1247,7 +1248,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
controlClassName="w-full"
|
||||
>
|
||||
<Select value={monoFont} onValueChange={(value) => setMonoFont(value as MonoFontOption)}>
|
||||
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectCodeFontAria')} size={SETTINGS_SELECT_SIZE} className={cn(SETTINGS_SELECT_TRIGGER_CLASS, 'max-w-full')}>
|
||||
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectCodeFontAria')} size={SETTINGS_SELECT_SIZE} className={SETTINGS_SELECT_TRIGGER_CLASS}>
|
||||
<SelectValue>{CODE_FONT_OPTIONS.find((option) => option.id === monoFont)?.label}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -1288,7 +1289,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
min={50}
|
||||
max={200}
|
||||
step={5}
|
||||
className="w-20"
|
||||
className={SETTINGS_NUMBER_INPUT_CLASS}
|
||||
aria-label={t('settings.openchamber.visual.field.fontSizePercentageAria')}
|
||||
/>
|
||||
<span className={SETTINGS_NUMBER_UNIT_CLASS}>%</span>
|
||||
@@ -1319,7 +1320,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
min={9}
|
||||
max={52}
|
||||
step={1}
|
||||
className="w-20"
|
||||
className={SETTINGS_NUMBER_INPUT_CLASS}
|
||||
/>
|
||||
<span className={SETTINGS_NUMBER_UNIT_CLASS}>px</span>
|
||||
<Button size="sm"
|
||||
@@ -1349,7 +1350,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
min={9}
|
||||
max={32}
|
||||
step={1}
|
||||
className="w-20"
|
||||
className={SETTINGS_NUMBER_INPUT_CLASS}
|
||||
/>
|
||||
<span className={SETTINGS_NUMBER_UNIT_CLASS}>px</span>
|
||||
<Button size="sm"
|
||||
@@ -1384,7 +1385,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
min={50}
|
||||
max={200}
|
||||
step={5}
|
||||
className="w-20"
|
||||
className={SETTINGS_NUMBER_INPUT_CLASS}
|
||||
/>
|
||||
<span className={SETTINGS_NUMBER_UNIT_CLASS}>%</span>
|
||||
<Button size="sm"
|
||||
@@ -1415,7 +1416,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
className="w-20"
|
||||
className={SETTINGS_NUMBER_INPUT_CLASS}
|
||||
/>
|
||||
<span className={SETTINGS_NUMBER_UNIT_CLASS}>px</span>
|
||||
<Button size="sm"
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
SettingsChipGroup,
|
||||
SettingsInset,
|
||||
SETTINGS_ICON_BUTTON_CLASS,
|
||||
SETTINGS_NUMBER_INPUT_CLASS,
|
||||
} from '@/components/sections/shared/SettingsSection';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
|
||||
import { useI18n, type I18nKey } from '@/lib/i18n';
|
||||
@@ -87,7 +89,7 @@ export const SessionRetentionSettings: React.FC = () => {
|
||||
max={MAX_DAYS}
|
||||
step={1}
|
||||
aria-label={t('settings.openchamber.sessionRetention.field.retentionPeriodAria')}
|
||||
className="w-24 tabular-nums"
|
||||
className={cn(SETTINGS_NUMBER_INPUT_CLASS, 'tabular-nums')}
|
||||
/>
|
||||
<span className="typography-ui-label text-muted-foreground">{t('settings.openchamber.sessionRetention.field.days')}</span>
|
||||
<Button
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
SettingsControlGroup,
|
||||
SettingsChipGroup,
|
||||
SETTINGS_SELECT_SIZE,
|
||||
SETTINGS_NUMBER_INPUT_CLASS,
|
||||
SETTINGS_SELECT_ROW_TRIGGER_CLASS,
|
||||
SETTINGS_CONTROL_CLUSTER_CLASS,
|
||||
SETTINGS_FIELD_LABEL_CLASS,
|
||||
@@ -1051,20 +1052,20 @@ export const VoiceSettings: React.FC = () => {
|
||||
{/* Speech Rate */}
|
||||
<SettingsFieldRow label={t('settings.voice.page.field.speechRate')}>
|
||||
{!isMobile && <input type="range" min={0.5} max={2} step={0.1} value={speechRate} onChange={(e) => setSpeechRate(Number(e.target.value))} className={sliderClass} />}
|
||||
<NumberInput value={speechRate} onValueChange={setSpeechRate} min={0.5} max={2} step={0.1} className="w-20 tabular-nums" />
|
||||
<NumberInput value={speechRate} onValueChange={setSpeechRate} min={0.5} max={2} step={0.1} className={cn(SETTINGS_NUMBER_INPUT_CLASS, 'tabular-nums')} />
|
||||
</SettingsFieldRow>
|
||||
|
||||
{/* Speech Pitch */}
|
||||
<SettingsFieldRow label={t('settings.voice.page.field.speechPitch')}>
|
||||
{!isMobile && <input type="range" min={0.5} max={2} step={0.1} value={speechPitch} onChange={(e) => setSpeechPitch(Number(e.target.value))} className={sliderClass} />}
|
||||
<NumberInput value={speechPitch} onValueChange={setSpeechPitch} min={0.5} max={2} step={0.1} className="w-20 tabular-nums" />
|
||||
<NumberInput value={speechPitch} onValueChange={setSpeechPitch} min={0.5} max={2} step={0.1} className={cn(SETTINGS_NUMBER_INPUT_CLASS, 'tabular-nums')} />
|
||||
</SettingsFieldRow>
|
||||
|
||||
{/* Speech Volume */}
|
||||
<SettingsFieldRow label={t('settings.voice.page.field.speechVolume')}>
|
||||
{!isMobile && <input type="range" min={0} max={1} step={0.1} value={speechVolume} onChange={(e) => setSpeechVolume(Number(e.target.value))} className={sliderClass} />}
|
||||
{isMobile ? (
|
||||
<NumberInput value={Math.round(speechVolume * 100)} onValueChange={(v) => setSpeechVolume(v / 100)} min={0} max={100} step={10} className="w-16 tabular-nums" />
|
||||
<NumberInput value={Math.round(speechVolume * 100)} onValueChange={(v) => setSpeechVolume(v / 100)} min={0} max={100} step={10} className={cn(SETTINGS_NUMBER_INPUT_CLASS, 'tabular-nums')} />
|
||||
) : (
|
||||
<span className="typography-ui-label text-foreground tabular-nums min-w-[3rem] text-right">
|
||||
{Math.round(speechVolume * 100)}%
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
SETTINGS_SECTION_TITLE_CLASS,
|
||||
SETTINGS_FIELD_LABEL_CLASS,
|
||||
SETTINGS_SELECT_SIZE,
|
||||
SETTINGS_NUMBER_INPUT_CLASS,
|
||||
} from '@/components/sections/shared/SettingsSection';
|
||||
import { SettingsInfoHint } from '@/components/sections/shared/SettingsInfoHint';
|
||||
import { useDesktopSshStore } from '@/stores/useDesktopSshStore';
|
||||
@@ -2322,7 +2323,7 @@ export const RemoteInstancesPage: React.FC = () => {
|
||||
min={5}
|
||||
max={240}
|
||||
step={1}
|
||||
className="w-20 tabular-nums"
|
||||
className={cn(SETTINGS_NUMBER_INPUT_CLASS, 'tabular-nums')}
|
||||
value={draft.connectionTimeoutSec}
|
||||
onValueChange={(next) => {
|
||||
updateDraft((current) => ({
|
||||
@@ -2350,7 +2351,7 @@ export const RemoteInstancesPage: React.FC = () => {
|
||||
min={1}
|
||||
max={65535}
|
||||
step={1}
|
||||
className="w-32 tabular-nums"
|
||||
className={cn(SETTINGS_NUMBER_INPUT_CLASS, 'tabular-nums')}
|
||||
value={draft.remoteOpenchamber.preferredPort}
|
||||
onValueChange={(next) => {
|
||||
updateDraft((current) => ({
|
||||
@@ -2517,7 +2518,7 @@ export const RemoteInstancesPage: React.FC = () => {
|
||||
min={1}
|
||||
max={65535}
|
||||
step={1}
|
||||
className="w-32 tabular-nums"
|
||||
className={cn(SETTINGS_NUMBER_INPUT_CLASS, 'tabular-nums')}
|
||||
value={draft.localForward.preferredLocalPort}
|
||||
onValueChange={(next) => {
|
||||
updateDraft((current) => ({
|
||||
@@ -2775,7 +2776,7 @@ export const RemoteInstancesPage: React.FC = () => {
|
||||
min={1}
|
||||
max={65535}
|
||||
step={1}
|
||||
className="w-32 tabular-nums"
|
||||
className={cn(SETTINGS_NUMBER_INPUT_CLASS, 'tabular-nums')}
|
||||
value={forward.localPort}
|
||||
onValueChange={(next) => {
|
||||
updateForward((item) => ({
|
||||
@@ -2817,7 +2818,7 @@ export const RemoteInstancesPage: React.FC = () => {
|
||||
min={1}
|
||||
max={65535}
|
||||
step={1}
|
||||
className="w-32 tabular-nums"
|
||||
className={cn(SETTINGS_NUMBER_INPUT_CLASS, 'tabular-nums')}
|
||||
value={forward.remotePort}
|
||||
onValueChange={(next) => {
|
||||
updateForward((item) => ({
|
||||
|
||||
@@ -6,12 +6,33 @@ import { dropdownTriggerVariants } from '@/components/ui/dropdown-trigger';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SettingsInfoHint } from './SettingsInfoHint';
|
||||
|
||||
/**
|
||||
* Width cap shared by every settings value picker.
|
||||
*
|
||||
* `applyTypography` scales the `--typography-*` vars but never the root rem,
|
||||
* so a rem-based cap (`max-w-48`) stays 192px while the label inside it grows,
|
||||
* and the value clips at 150–200% interface font size. `ch` is measured
|
||||
* against the trigger's own `typography-ui-label` font, so the cap grows with
|
||||
* the setting. Below `@xl` the field row stacks and the control is plain
|
||||
* `w-full`, so this cap only binds on wide panes.
|
||||
*/
|
||||
const SETTINGS_TRIGGER_WIDTH_CLASS = 'w-full min-w-[22ch] max-w-[40ch]';
|
||||
|
||||
/** Settings select trigger: full column width in stacked cells; capped in field rows via parent. */
|
||||
export const SETTINGS_SELECT_TRIGGER_CLASS = 'w-full min-w-40 max-w-48';
|
||||
export const SETTINGS_SELECT_TRIGGER_CLASS = SETTINGS_TRIGGER_WIDTH_CLASS;
|
||||
export const SETTINGS_SELECT_SIZE = 'settings' as const;
|
||||
|
||||
/** Fixed-width select used inside full-width SettingsFieldRow control columns. */
|
||||
export const SETTINGS_SELECT_ROW_TRIGGER_CLASS = 'w-full min-w-40 max-w-48';
|
||||
export const SETTINGS_SELECT_ROW_TRIGGER_CLASS = SETTINGS_TRIGGER_WIDTH_CLASS;
|
||||
|
||||
/**
|
||||
* Width for every settings NumberInput stepper. Font-relative for the same
|
||||
* reason as the trigger cap above: a rem-fixed `w-16`/`w-20`/`w-24`/`w-32`
|
||||
* clips its own digits once the interface font is scaled up. The explicit
|
||||
* `typography-ui-label` pins `ch` to the same scaled font the inner numeric
|
||||
* field renders in, since the wrapper would otherwise inherit an unscaled one.
|
||||
*/
|
||||
export const SETTINGS_NUMBER_INPUT_CLASS = 'typography-ui-label w-[16ch]';
|
||||
|
||||
/** Compact reset / icon action next to a settings control (matches h-8 controls). */
|
||||
export const SETTINGS_ICON_BUTTON_CLASS =
|
||||
@@ -21,7 +42,7 @@ export const SETTINGS_ICON_BUTTON_CLASS =
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const SETTINGS_CUSTOM_TRIGGER_CLASS = cn(
|
||||
dropdownTriggerVariants(),
|
||||
'w-full min-w-40 max-w-48',
|
||||
SETTINGS_TRIGGER_WIDTH_CLASS,
|
||||
);
|
||||
|
||||
/** Shared width for stacked control clusters (select/input + reset). */
|
||||
|
||||
Reference in New Issue
Block a user