qwen-code/packages/web-shell/client/components/dialogs/ThemeDialog.tsx
ytahdn c61006b978
feat(web-shell): daemon web-shell improvements — token usage, settings, retry, streaming metrics, hidden commands (#5066)
* 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>
2026-06-13 02:58:08 +00:00

127 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}