qwen-code/packages/web-shell/client/components/dialogs/ApprovalModeDialog.tsx
ermin.zem 5c82857fea
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
Add harness infrastructure for web-shell package (#6517)
* 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>
2026-07-09 08:11:58 +00:00

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