qwen-code/packages/web-shell/client/components/dialogs/ResumeDialog.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

145 lines
4.4 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
import { dp } from './dialogStyles';
import { useConnection, useSessions } from '@qwen-code/webui/daemon-react-sdk';
import { useI18n } from '../../i18n';
import { useListboxKeyboard } from '../../hooks/useListboxKeyboard';
import { useFilterInput } from '../../hooks/useFilterInput';
import { SessionRow } from './SessionRow';
interface ResumeDialogProps {
onSelect: (sessionId: string) => void;
onClose: () => void;
}
const LIST_ID = 'resume-session-list';
const optionId = (index: number) => `${LIST_ID}-opt-${index}`;
export function ResumeDialog({ onSelect, onClose }: ResumeDialogProps) {
const { t } = useI18n();
const connection = useConnection();
const { sessions, loading, error } = useSessions({ autoLoad: true });
const currentSessionId = connection.sessionId;
// -1 = no highlight. The dialog opens with nothing highlighted and resets to
// none on filter edits, so Enter in the search box cannot confirm a row the
// user didn't pick — the highlight only appears once they press ↓/↑ or hover.
const [selectedIdx, setSelectedIdx] = useState(-1);
const { filterValue: filterQuery, inputProps } = useFilterInput(() =>
setSelectedIdx(-1),
);
const listRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const filtered = filterQuery
? sessions.filter((s) => {
const q = filterQuery.toLowerCase();
return (
(s.displayName || '').toLowerCase().includes(q) ||
s.sessionId.toLowerCase().includes(q)
);
})
: sessions;
// Keep selection in bounds
useEffect(() => {
if (selectedIdx >= filtered.length && filtered.length > 0) {
setSelectedIdx(filtered.length - 1);
}
}, [filtered.length, selectedIdx]);
// Scroll selected item into view
useEffect(() => {
const el = listRef.current?.children[selectedIdx] as
| HTMLElement
| undefined;
el?.scrollIntoView({ block: 'nearest' });
}, [selectedIdx]);
const confirm = (index: number) => {
const session = filtered[index];
if (!session) return;
onSelect(session.sessionId);
onClose();
};
const { keyboardMode } = useListboxKeyboard({
itemCount: filtered.length,
activeIndex: selectedIdx,
onActiveIndexChange: setSelectedIdx,
onConfirm: confirm,
});
useEffect(() => {
inputRef.current?.focus();
}, []);
return (
<div
className={dp('picker', 'picker-in-shell')}
data-web-shell-resume-dialog
>
<div className={dp('picker-search')}>
<span className={dp('picker-search-label')}>
{t('resume.search')}:{' '}
</span>
<input
ref={inputRef}
className={dp('picker-search-input')}
aria-label={t('resume.search')}
role="combobox"
aria-autocomplete="list"
aria-expanded="true"
aria-controls={LIST_ID}
aria-activedescendant={
selectedIdx >= 0 && selectedIdx < filtered.length
? optionId(selectedIdx)
: undefined
}
{...inputProps}
placeholder=""
/>
</div>
<div className={dp('picker-sep')} />
<div
id={LIST_ID}
role="listbox"
className={dp(
'picker-list',
keyboardMode ? 'picker-keyboard-only' : undefined,
)}
ref={listRef}
>
{loading && (
<div className={dp('picker-empty')}>{t('common.loading')}</div>
)}
{!loading && error && (
<div className={dp('picker-empty')}>
{error.message || t('resume.failedToLoad')}
</div>
)}
{!loading && !error && filtered.length === 0 && (
<div className={dp('picker-empty')}>
{filterQuery
? t('resume.noMatch', { query: filterQuery })
: t('resume.none')}
</div>
)}
{!loading &&
filtered.map((s, index) => (
<SessionRow
key={s.sessionId}
session={s}
optionId={optionId(index)}
active={index === selectedIdx}
current={s.sessionId === currentSessionId}
currentLabel={t('resume.current')}
resumeSelector
onClick={() => confirm(index)}
onActivate={() => setSelectedIdx(index)}
/>
))}
</div>
</div>
);
}