mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-04 05:40:58 +00:00
Some checks are pending
* test(web-shell): add browser and lint harness * test(web-shell): harden browser smoke harness * fix(web-shell): guard mock daemon model state * test(web-shell): remove unused scenario harness * fix(web-shell): remove stale lint disables * test(web-shell): make matchMedia stub writable * fix(web-shell): exclude tests from package typecheck * test(web-shell): tighten mock daemon route contract * Update packages/web-shell/client/e2e/utils/mockDaemon.ts Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com> * test(web-shell): clear stale SSE connections * ci(web-shell): gate smoke on full CI profile --------- Co-authored-by: ermin.zem <ermin.zem@alibaba-inc.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: 易良 <1204183885@qq.com>
113 lines
3.5 KiB
TypeScript
113 lines
3.5 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { DAEMON_APPROVAL_MODES } from '@qwen-code/webui/daemon-react-sdk';
|
|
import { useI18n } from '../../i18n';
|
|
import { useListboxKeyboard } from '../../hooks/useListboxKeyboard';
|
|
import { dp } from './dialogStyles';
|
|
import { ModeIcon } from '../ModeIcon';
|
|
import styles from './ApprovalModeDialog.module.css';
|
|
|
|
interface ApprovalModeDialogProps {
|
|
currentMode: string;
|
|
onSelect: (modeId: string) => void;
|
|
}
|
|
|
|
interface ModeItem {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
}
|
|
|
|
export function ApprovalModeDialog({
|
|
currentMode,
|
|
onSelect,
|
|
}: ApprovalModeDialogProps) {
|
|
const { t } = useI18n();
|
|
const listRef = useRef<HTMLDivElement>(null);
|
|
const approvalModes: ModeItem[] = DAEMON_APPROVAL_MODES.map((id) => ({
|
|
id,
|
|
name: t(`mode.listLabel.${id}`),
|
|
description: t(`mode.desc.${id}`),
|
|
}));
|
|
|
|
const currentIdx = approvalModes.findIndex((m) => m.id === currentMode);
|
|
const [activeIndex, setActiveIndex] = useState(
|
|
currentIdx >= 0 ? currentIdx : 0,
|
|
);
|
|
|
|
// Follow the current mode until the user first navigates: it can change
|
|
// while the dialog is open (e.g. another client sharing the session flips
|
|
// approval mode). Once the user has moved the highlight, don't steal it.
|
|
const userNavigatedRef = useRef(false);
|
|
useEffect(() => {
|
|
if (userNavigatedRef.current || currentIdx < 0) return;
|
|
setActiveIndex(currentIdx);
|
|
}, [currentIdx]);
|
|
|
|
const moveHighlight = (index: number) => {
|
|
userNavigatedRef.current = true;
|
|
setActiveIndex(index);
|
|
};
|
|
|
|
const confirm = (index: number) => {
|
|
const mode = approvalModes[index];
|
|
if (mode) onSelect(mode.id);
|
|
};
|
|
|
|
const { keyboardMode } = useListboxKeyboard({
|
|
itemCount: approvalModes.length,
|
|
activeIndex,
|
|
onActiveIndexChange: moveHighlight,
|
|
onConfirm: confirm,
|
|
});
|
|
|
|
useEffect(() => {
|
|
const el = listRef.current?.children[activeIndex] as
|
|
| HTMLElement
|
|
| undefined;
|
|
el?.scrollIntoView({ block: 'nearest' });
|
|
}, [activeIndex]);
|
|
|
|
return (
|
|
<div
|
|
className={`${styles.list} ${keyboardMode ? styles.keyboardOnly : ''}`}
|
|
ref={listRef}
|
|
role="listbox"
|
|
tabIndex={0}
|
|
aria-activedescendant={
|
|
approvalModes.length > 0 ? `mode-opt-${activeIndex}` : undefined
|
|
}
|
|
aria-label={t('mode.select')}
|
|
data-web-shell-approval-mode-dialog
|
|
>
|
|
{approvalModes.map((mode, index) => {
|
|
const selected = index === activeIndex;
|
|
const isCurrent = mode.id === currentMode;
|
|
return (
|
|
<div
|
|
key={mode.id}
|
|
id={`mode-opt-${index}`}
|
|
role="option"
|
|
// aria-selected marks the actual current mode; the roving keyboard
|
|
// highlight is conveyed by aria-activedescendant + `.selected`.
|
|
aria-selected={isCurrent}
|
|
className={`${styles.row} ${selected ? styles.selected : ''} ${
|
|
isCurrent ? dp('dialog-current') : ''
|
|
}`}
|
|
data-web-shell-approval-mode-option
|
|
data-mode-id={mode.id}
|
|
onClick={() => confirm(index)}
|
|
onMouseMove={() => moveHighlight(index)}
|
|
>
|
|
<span className={styles.modeIcon}>
|
|
<ModeIcon mode={mode.id} />
|
|
</span>
|
|
<span className={styles.modeText}>
|
|
<span className={styles.modeName}>{mode.name}</span>
|
|
<span className={styles.modeDesc}>{mode.description}</span>
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|