From df4e7907b6dd93fc98135d70b2cf9f82ef6ca0a6 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Tue, 22 Sep 2026 20:43:34 +0800 Subject: [PATCH] fix(desktop): make host message queue compact and readable --- .../components/HostPendingQueuePanel.scss | 36 +++++++++++ .../components/HostPendingQueuePanel.test.tsx | 37 +++++++++++ .../components/HostPendingQueuePanel.tsx | 62 +++++++++++-------- src/web-ui/src/locales/en-US/flow-chat.json | 3 +- src/web-ui/src/locales/zh-CN/flow-chat.json | 3 +- src/web-ui/src/locales/zh-TW/flow-chat.json | 3 +- 6 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 src/web-ui/src/flow_chat/components/HostPendingQueuePanel.scss diff --git a/src/web-ui/src/flow_chat/components/HostPendingQueuePanel.scss b/src/web-ui/src/flow_chat/components/HostPendingQueuePanel.scss new file mode 100644 index 0000000000..c361b0232a --- /dev/null +++ b/src/web-ui/src/flow_chat/components/HostPendingQueuePanel.scss @@ -0,0 +1,36 @@ +.host-pending-queue { + padding: var(--openbitfun-space-2) var(--openbitfun-space-3); + gap: var(--openbitfun-space-1); + font-size: var(--openbitfun-type-body-sm-font-size); + + [data-openbitfun-component='chat-composer-queue'][data-openbitfun-part='header'] { + justify-content: space-between; + } + &__toggle { padding: 0; color: var(--openbitfun-color-content-secondary); } + &__note, &__error { + margin: 0; + font-size: var(--openbitfun-type-body-sm-font-size); + line-height: var(--openbitfun-type-body-sm-line-height); + color: var(--openbitfun-color-content-secondary); + overflow-wrap: anywhere; + } + [data-openbitfun-component='chat-composer-queue'][data-openbitfun-part='list'][hidden] { display: none; } + [data-openbitfun-component='chat-composer-queue'][data-openbitfun-part='item'] { + flex-shrink: 0; + grid-template-columns: minmax(0, 1fr) auto; + } + [data-openbitfun-component='chat-composer-queue'][data-openbitfun-part='content'] { + font-size: var(--openbitfun-type-body-sm-font-size); + line-height: var(--openbitfun-type-body-sm-line-height); + gap: 0; + } + [data-openbitfun-component='chat-composer-queue'][data-openbitfun-part='actions'] { + opacity: 1; + pointer-events: auto; + gap: var(--openbitfun-space-1); + } + &__status { + font-size: var(--openbitfun-type-meta-font-size); + color: var(--openbitfun-color-content-muted); + } +} diff --git a/src/web-ui/src/flow_chat/components/HostPendingQueuePanel.test.tsx b/src/web-ui/src/flow_chat/components/HostPendingQueuePanel.test.tsx index 348b570de4..ecc2c13852 100644 --- a/src/web-ui/src/flow_chat/components/HostPendingQueuePanel.test.tsx +++ b/src/web-ui/src/flow_chat/components/HostPendingQueuePanel.test.tsx @@ -56,3 +56,40 @@ it.each([false, true])('keeps a normal send hidden and shows recovery only on fa container.remove(); } }); + +it('keeps four queued messages compact, exposes actions, and toggles help independently', async () => { + const requests: string[] = []; + let items = Array.from({ length: 4 }, (_, index) => ({ turnId: String(index), displayContent: String(index + 1), + status: 'queued' as const, previewTruncated: false, attachmentCount: 0, agentType: 'Standard', + createdAtMs: 1, reason: null, targetTurnId: null, steeringId: null })); + const queue = new HostDialogQueue('scope', 'session', async request => { + requests.push(request.action); + if (request.action === 'cancel' || request.action === 'promote') items = items.filter(item => item.turnId !== request.turnId); + return { sessionId: 'session', queueEpoch: 'epoch', revision: requests.length, activeTurnId: 'active', + items, capacity: 20, used: items.length, receipt: null }; + }, { list: async () => [], put: async () => {}, remove: async () => {} }); + const container = document.createElement('div'); + document.body.append(container); + const root = createRoot(container); + try { + await act(async () => root.render( true} />)); + expect(container.querySelectorAll('li')).toHaveLength(4); + expect(container.textContent).not.toContain('hostQueue.memoryNotice'); + expect(container.textContent).not.toContain('hostQueue.queued'); + expect(container.querySelectorAll('[aria-label="hostQueue.sendNow"]')).toHaveLength(4); + const toggle = container.querySelector('.host-pending-queue__toggle')!; + await act(async () => toggle.click()); + expect(container.querySelector('ul')?.hidden).toBe(true); + await act(async () => container.querySelector('[aria-label="hostQueue.about"]')!.click()); + expect(container.textContent).toContain('hostQueue.memoryNotice'); + await act(async () => toggle.click()); + await act(async () => container.querySelector('[aria-label="hostQueue.sendNow"]')!.click()); + expect(requests).toContain('promote'); + expect(container.querySelectorAll('li')).toHaveLength(3); + await act(async () => container.querySelector('[aria-label="hostQueue.cancel"]')!.click()); + expect(container.querySelectorAll('li')).toHaveLength(2); + } finally { + await act(async () => root.unmount()); + container.remove(); + } +}); diff --git a/src/web-ui/src/flow_chat/components/HostPendingQueuePanel.tsx b/src/web-ui/src/flow_chat/components/HostPendingQueuePanel.tsx index fb99a31448..874eb6f147 100644 --- a/src/web-ui/src/flow_chat/components/HostPendingQueuePanel.tsx +++ b/src/web-ui/src/flow_chat/components/HostPendingQueuePanel.tsx @@ -1,8 +1,10 @@ import { pendingQueueManager } from '../services/flow-chat-manager/PendingQueueModule'; import type { QueuedMessage } from '../types/flow-chat'; -import { useEffect, useState, useSyncExternalStore } from 'react'; +import { useEffect, useId, useState, useSyncExternalStore } from 'react'; import { useI18n } from '@/infrastructure/i18n'; -import { Button } from '@openbitfun/ui'; +import { Button, IconButton, OverflowText, Tooltip } from '@openbitfun/ui'; +import { ArrowUp, ChevronDown, ChevronUp, Info, Pencil, RotateCw, X } from 'lucide-react'; +import './HostPendingQueuePanel.scss'; import { HostDialogQueue, observeHostQueue, type QueueOutboxRecord, type QueueItem } from '../../../../shared/dialog-queue/HostDialogQueue'; import { ChatComposerQueue, ChatComposerQueueHeader, ChatComposerQueueTitle, @@ -12,6 +14,9 @@ import { export function HostPendingQueuePanel({ queue, onRestore }: { queue: HostDialogQueue; onRestore: (item: QueuedMessage) => boolean }) { const { t } = useI18n('flow-chat'); const view = useSyncExternalStore(queue.subscribe, queue.getSnapshot, queue.getSnapshot); + const [expanded, setExpanded] = useState(true); + const [showHelp, setShowHelp] = useState(false); + const listId = useId(); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); useEffect(() => observeHostQueue(queue), [queue]); @@ -47,37 +52,44 @@ export function HostPendingQueuePanel({ queue, onRestore }: { queue: HostDialogQ const items = view.snapshot?.items ?? []; const visibleError = error || (view.error?.includes('Session is not loaded') ? null : view.error); if (!items.length && !view.pending.length && !visibleError) return null; - return - {t('hostQueue.title')} -

{t('hostQueue.memoryNotice')}

- {visibleError &&
{visibleError}
} - - {items.map(item => - -
{item.displayContent}
- {item.status === 'blocked' ? t('hostQueue.blocked') : item.status === 'steering_pending' ? t('hostQueue.steeringPending') : t('hostQueue.queued')} - {item.attachmentCount > 0 && {t('hostQueue.attachments', { count: item.attachmentCount })}} - {item.reason &&

{item.reason}

} + return + + + } onClick={() => setShowHelp(value => !value)} /> + + {showHelp &&

{t('hostQueue.memoryNotice')}

} + {visibleError &&
{visibleError}
} + diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index 15b919ab8f..415b6c9782 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -2881,7 +2881,8 @@ "loading": "Opening assistant…" }, "hostQueue": { - "title": "Host message queue", + "title": "Queued messages", + "about": "About queued messages", "memoryNotice": "Accepted messages run while this page is closed. Restarting the execution device clears pending messages.", "queued": "Queued", "blocked": "Waiting for recovery", diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index 3980762c96..49077384fc 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -2881,7 +2881,8 @@ "loading": "正在打开助手…" }, "hostQueue": { - "title": "宿主消息队列", + "title": "待发送", + "about": "排队消息说明", "memoryNotice": "消息接受后,关闭此页面仍会执行。执行设备重启会清空待执行消息。", "queued": "排队中", "blocked": "等待恢复", diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index 714fd03da9..ffef18db20 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -2881,7 +2881,8 @@ "loading": "正在開啟助手…" }, "hostQueue": { - "title": "主機訊息佇列", + "title": "待傳送", + "about": "排隊訊息說明", "memoryNotice": "訊息接受後,關閉此頁面仍會執行。執行裝置重新啟動會清空待執行訊息。", "queued": "排隊中", "blocked": "等待恢復",