mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
* feat(web-shell): daemon web-shell improvements - Align daemon token usage with structured DaemonTokenUsage type - Optimize settings panel with i18n, theme/language pickers, compact mode - Handle missing session recovery (404/410) with configurable behavior - Restore settings event signal bump for workspace changes - Prevent queued prompt loss on useEffect dependency change - Align streaming loading indicator with CLI metrics logic - Add Ctrl+Y retry for turn_error with daemon support - Hide non-essential UI elements on narrow screens (≤700px) - Prevent loading indicator flicker on page refresh - Hydrate displayName from persisted session title on load * fix(web-shell): harden retry affordance * fix(web-shell): gate retry handling --------- Co-authored-by: ytahdn <ytahdn@gmail.com>
127 lines
3.7 KiB
TypeScript
127 lines
3.7 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
||
import { dp } from './dialogStyles';
|
||
import { useDelayedGlobalKeyDown } from '../../hooks/useDelayedGlobalKeyDown';
|
||
import { useI18n } from '../../i18n';
|
||
import { WEB_SHELL_THEMES, type WebShellTheme } from '../../themeContext';
|
||
|
||
interface ThemeDialogProps {
|
||
currentTheme: WebShellTheme;
|
||
onSelect: (theme: WebShellTheme) => void;
|
||
onClose: () => void;
|
||
}
|
||
|
||
export function ThemeDialog({
|
||
currentTheme,
|
||
onSelect,
|
||
onClose,
|
||
}: ThemeDialogProps) {
|
||
const { t } = useI18n();
|
||
const themes = WEB_SHELL_THEMES.map((id) => ({
|
||
id,
|
||
label: t(`theme.${id}`),
|
||
description: t(`theme.${id}.desc`),
|
||
}));
|
||
const [selectedIdx, setSelectedIdx] = useState(() => {
|
||
const idx = themes.findIndex((theme) => theme.id === currentTheme);
|
||
return idx >= 0 ? idx : 0;
|
||
});
|
||
const listRef = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
const el = listRef.current?.children[selectedIdx] as
|
||
| HTMLElement
|
||
| undefined;
|
||
el?.scrollIntoView({ block: 'nearest' });
|
||
}, [selectedIdx]);
|
||
|
||
const handleSelect = useCallback(() => {
|
||
const theme = themes[selectedIdx];
|
||
if (theme) {
|
||
onSelect(theme.id);
|
||
onClose();
|
||
}
|
||
}, [onClose, onSelect, selectedIdx, themes]);
|
||
|
||
useDelayedGlobalKeyDown(
|
||
(event: KeyboardEvent) => {
|
||
if (event.key === 'Escape') {
|
||
event.preventDefault();
|
||
onClose();
|
||
return;
|
||
}
|
||
if (event.key === 'ArrowDown' || event.key === 'j') {
|
||
event.preventDefault();
|
||
setSelectedIdx((index) => Math.min(index + 1, themes.length - 1));
|
||
return;
|
||
}
|
||
if (event.key === 'ArrowUp' || event.key === 'k') {
|
||
event.preventDefault();
|
||
setSelectedIdx((index) => Math.max(index - 1, 0));
|
||
return;
|
||
}
|
||
if (event.key === 'Enter') {
|
||
event.preventDefault();
|
||
handleSelect();
|
||
}
|
||
},
|
||
[handleSelect, onClose, themes.length],
|
||
);
|
||
|
||
return (
|
||
<div className={dp('resume-picker')}>
|
||
<div className={dp('resume-picker-header')}>
|
||
<span className={dp('resume-picker-title')}>{t('theme.title')}</span>
|
||
<span className={dp('resume-picker-count')}>
|
||
{t('theme.current', { theme: currentTheme })}
|
||
</span>
|
||
<button
|
||
className={dp('resume-picker-close')}
|
||
onClick={onClose}
|
||
title="Close"
|
||
>
|
||
ESC
|
||
</button>
|
||
</div>
|
||
|
||
<div className={dp('resume-picker-sep')} />
|
||
|
||
<div className={dp('resume-picker-list')} ref={listRef}>
|
||
{themes.map((theme, index) => (
|
||
<div
|
||
key={theme.id}
|
||
className={dp(
|
||
'resume-picker-item',
|
||
index === selectedIdx ? 'selected' : undefined,
|
||
)}
|
||
onClick={() => {
|
||
onSelect(theme.id);
|
||
onClose();
|
||
}}
|
||
onMouseEnter={() => setSelectedIdx(index)}
|
||
>
|
||
<div className={dp('resume-picker-item-row')}>
|
||
<span className={dp('resume-picker-item-prefix')}>
|
||
{index === selectedIdx ? '›' : ' '}
|
||
</span>
|
||
<span className={dp('resume-picker-item-title')}>
|
||
{theme.label}
|
||
</span>
|
||
{theme.id === currentTheme && (
|
||
<span className={dp('resume-picker-item-check')}> ✓</span>
|
||
)}
|
||
</div>
|
||
<div className={dp('resume-picker-item-meta')}>
|
||
{theme.description}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className={dp('resume-picker-sep')} />
|
||
|
||
<div className={dp('resume-picker-footer')}>
|
||
{t('dialog.footer.navSelectCancel')}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|