/** * @license * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ import type { PromptImage } from '../adapters/promptTypes'; import type { DaemonInputAnnotation } from '@qwen-code/sdk/daemon'; import { Fragment } from 'react'; 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 { useWebShellCustomization, type UserMessageContentParser, type WebShellComposerTag, } from '../customization'; import { parseUserMessageContentSafely, splitComposerTagContentByAnnotations, } from '../utils/composerTag'; import { cssUrlVar } from '../utils/cssUrlVar'; import { isCommandPrompt } from '../utils/localCommandQueue'; import { ReadonlyComposerTag } from './messages/UserMessage'; import styles from '../App.module.css'; const MAX_QUEUED_PROMPT_PREVIEW_CHARS = 240; type QueuedPromptPreviewPart = | { type: 'text'; text: string } | { type: 'tag'; tag: WebShellComposerTag; preserveCustomKindLabel: boolean; }; function getTagDisplayText(tag: WebShellComposerTag): string { return tag.value?.trim() || tag.label?.trim() || tag.id; } function getQueuedPromptParts( prompt: QueuedPrompt, parser: UserMessageContentParser | undefined, ): QueuedPromptPreviewPart[] { if (prompt.inputAnnotations && prompt.inputAnnotations.length > 0) { return splitComposerTagContentByAnnotations( prompt.text, prompt.inputAnnotations, ).map((segment) => segment.type === 'text' ? segment : { type: 'tag', tag: segment.tag, preserveCustomKindLabel: true, }, ); } const parsed = parseUserMessageContentSafely( prompt.text, parser, '[WebShell] failed to parse queued prompt content', { requireSourcePreservation: true }, ); if (!parsed) return [{ type: 'text', text: prompt.text }]; return parsed.map((part) => part.type === 'text' ? part : { type: 'tag', tag: part.tag, preserveCustomKindLabel: false }, ); } function truncateQueuedPromptParts(parts: readonly QueuedPromptPreviewPart[]): { parts: QueuedPromptPreviewPart[]; truncated: boolean; } { const preview: QueuedPromptPreviewPart[] = []; let remaining = MAX_QUEUED_PROMPT_PREVIEW_CHARS; let truncated = false; for (const part of parts) { if (part.type === 'tag') { if (remaining <= 0) { truncated = true; break; } const visibleLength = getTagDisplayText(part.tag).length; if (visibleLength > remaining) { truncated = true; break; } preview.push(part); remaining -= visibleLength; continue; } let text = part.text.replace(/\s+/g, ' '); if (preview.length === 0) text = text.trimStart(); if (!text) continue; if (text.length > remaining) { if (remaining > 0) preview.push({ type: 'text', text: text.slice(0, remaining) }); truncated = true; break; } preview.push({ type: 'text', text }); remaining -= text.length; } const last = preview[preview.length - 1]; if (last?.type === 'text') { const text = last.text.trimEnd(); if (text) last.text = text; else preview.pop(); } return { parts: preview, truncated }; } export interface QueuedPrompt { id: number; sessionId?: string; text: string; images?: PromptImage[]; inputAnnotations?: DaemonInputAnnotation[]; 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; onDelete: (id: number) => void; onInsert: (id: number) => void; onEdit: (id: number) => void; }) { const { parseUserMessageContent, composerTagIcons, renderComposerTag, renderComposerTagTooltip, onComposerTagClick, } = useWebShellCustomization(); if (prompts.length === 0) return null; return (
{prompts.map((prompt) => { const preview = truncateQueuedPromptParts( getQueuedPromptParts(prompt, parseUserMessageContent), ); 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 (
); })}
{t('queue.footer')}
); }