qwen-code/packages/web-shell/client/components/QueuedPromptDisplay.tsx
ytahdn 1609bdaa32
feat(web-shell): queue prompts while turns are running (#6005)
* feat(web-shell): queue prompts while turns are running

* fix(web-shell): address pending prompt review feedback

* fix(web-shell): tighten queued prompt event handling

* fix(web-shell): avoid showing active prompt as queued

* fix(web-shell): address queued prompt review follow-ups

* fix(web-shell): address pending prompt review issues

* fix(web-shell): reconcile queued prompt actions from server

* fix(daemon): address pending prompt critical review

* test(webui): expect submit abort signal forwarding

* fix(web-shell): avoid duplicate queued prompt sync

* fix(web-shell): keep local slash commands out of queue

* fix(webui): avoid aborting prompt admission

* fix(daemon): avoid queued prompt cancel cascades

* fix(webui): avoid stale client id for queue cleanup

* test(webui): update stale session queue cleanup expectation

* fix(web-shell): preserve queue reconciliation identity

* fix(web-shell): guard queue clear session writes

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-06-30 14:51:28 +00:00

150 lines
5.1 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type { PromptImage } from '../adapters/promptTypes';
import deleteIconUrl from '../assets/icons/delete.svg';
import editIconUrl from '../assets/icons/edit.svg';
import insertIconUrl from '../assets/icons/insert.svg';
import queueIconUrl from '../assets/icons/queue.svg';
import type { getTranslator } from '../i18n';
import { cssUrlVar } from '../utils/cssUrlVar';
import { isCommandPrompt } from '../utils/localCommandQueue';
import styles from '../App.module.css';
const MAX_QUEUED_PROMPT_PREVIEW_CHARS = 240;
export interface QueuedPrompt {
id: number;
sessionId?: string;
text: string;
images?: PromptImage[];
onComplete?: () => void;
serverPromptId?: string;
serverState?: 'submitting' | 'queued' | 'running';
isEditing?: boolean;
isRemoving?: boolean;
}
export function QueuedPromptDisplay({
prompts,
t,
onDelete,
onInsert,
onEdit,
}: {
prompts: readonly QueuedPrompt[];
t: ReturnType<typeof getTranslator>;
onDelete: (id: number) => void;
onInsert: (id: number) => void;
onEdit: (id: number) => void;
}) {
if (prompts.length === 0) return null;
return (
<div className={styles.queuedPrompts}>
{prompts.map((prompt) => {
const normalizedPreview = prompt.text.replace(/\s+/g, ' ').trim();
const preview =
normalizedPreview.length > MAX_QUEUED_PROMPT_PREVIEW_CHARS
? `${normalizedPreview.slice(0, MAX_QUEUED_PROMPT_PREVIEW_CHARS)}...`
: normalizedPreview;
const imageCount = prompt.images?.length ?? 0;
const isCommand = isCommandPrompt(prompt.text);
const isSubmitting = prompt.serverState === 'submitting';
const isRunning = prompt.serverState === 'running';
const isRemoving = prompt.isRemoving === true;
const isBusy =
isSubmitting || isRunning || prompt.isEditing === true || isRemoving;
let insertTitle = t('queue.insertTip');
if (isBusy) {
insertTitle = t('queue.submittingDisabled');
} else if (isCommand) {
insertTitle = t('queue.insertCommandDisabled');
}
let editTitle = t('queue.editTip');
if (isBusy) {
editTitle = t('queue.submittingDisabled');
}
const deleteTitle = isBusy
? t('queue.submittingDisabled')
: t('queue.deleteTip');
return (
<div key={prompt.id} className={styles.queuedPrompt}>
<span className={styles.queuedPromptIcon} aria-hidden="true">
<span
className={styles.queuedPromptMaskIcon}
style={cssUrlVar('--queued-icon-url', queueIconUrl)}
/>
</span>
<span className={styles.queuedPromptText}>
{preview}
{imageCount > 0
? ` ${t('queue.imageCount', { count: imageCount })}`
: ''}
{isSubmitting || prompt.isEditing || isRemoving ? (
<span className={styles.queuedPromptState}>
<span className={styles.queuedPromptSpinner} />
{isRemoving
? t('queue.removing')
: prompt.isEditing
? t('queue.editing')
: t('queue.submitting')}
</span>
) : null}
</span>
<span className={styles.queuedPromptActions}>
{imageCount === 0 && (
<button
type="button"
className={styles.queuedPromptAction}
onClick={() => onInsert(prompt.id)}
disabled={isCommand || isBusy}
title={insertTitle}
>
<span
className={styles.queuedPromptActionIcon}
style={cssUrlVar('--queued-icon-url', insertIconUrl)}
aria-hidden="true"
/>
{t('queue.insert')}
</button>
)}
<button
type="button"
className={styles.queuedPromptAction}
onClick={() => onDelete(prompt.id)}
disabled={isBusy}
aria-label={t('queue.delete')}
title={deleteTitle}
>
<span
className={styles.queuedPromptActionIcon}
style={cssUrlVar('--queued-icon-url', deleteIconUrl)}
aria-hidden="true"
/>
</button>
<button
type="button"
className={styles.queuedPromptAction}
onClick={() => onEdit(prompt.id)}
disabled={isBusy}
aria-label={t('queue.edit')}
title={editTitle}
>
<span
className={styles.queuedPromptActionIcon}
style={cssUrlVar('--queued-icon-url', editIconUrl)}
aria-hidden="true"
/>
</button>
</span>
</div>
);
})}
<div className={styles.queuedHint}>{t('queue.footer')}</div>
</div>
);
}