mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-21 06:35:07 +00:00
* 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>
150 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|