From 595f447bbd9ac3e33df1f7cdaf6039bfac01b78d Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 20 Sep 2026 15:40:23 +0800 Subject: [PATCH 01/33] fix(ui): make queued message delivery understandable Explain unconfirmed delivery and the messages waiting behind it without exposing Host terminology. Use labelled icon actions with tooltips and one body text size across queue content and status. Keep the existing Host admission, reconciliation identity and cancellation authority. Extend the existing controller, composer and recovery checks and reuse the queue story for unknown and failed delivery states. No new delivery state or scheduler is introduced. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Generated-by: Devin --- .../__tests__/message-queue-ui-state.test.ts | 6 +- .../controller/session-local-messages.tsx | 18 +- .../renderer/locales/session-local-copy.ts | 38 ++-- apps/desktop/src/renderer/styles/composer.css | 7 +- .../composer-message-queue.stories.tsx | 178 ++++++++++++++---- docs/astryx-surface-file-inventory.md | 2 +- .../__tests__/composer-send-toggle.test.tsx | 28 ++- packages/ui/src/chat-turn.tsx | 2 +- packages/ui/src/chat-view.tsx | 2 +- packages/ui/src/composer-message-queue.tsx | 21 ++- packages/ui/src/conversation-copy.ts | 12 +- 11 files changed, 232 insertions(+), 82 deletions(-) diff --git a/apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts b/apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts index 05f768b8ef5..02da87f4b05 100644 --- a/apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts +++ b/apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts @@ -65,19 +65,19 @@ test('local delivery recovery cannot republish accepted Host queue rows', async }) }), }))); assert.equal(transient.get('steering')?.deliveryActions?.length, 1, 'unconfirmed sends retain their receipt check'); - assert.equal(transient.get('root')?.deliveryStatus, 'Host outcome unknown'); + assert.equal(transient.get('root')?.deliveryStatus, 'Delivery unconfirmed. Do not send again.'); const placements = () => Object.fromEntries([...transient].map(([id, message]) => [id, message.transientPlacement])); assert.deepEqual(placements(), { steering: 'steering', followup: 'follow_up', root: 'transcript' }); messages = messages.map((message) => ({ ...message, state: 'saved', canCancel: true })); await act(async () => changed('session-1')); - assert.equal(transient.get('root')?.deliveryStatus, 'Saved locally · waiting to send', 'Main cannot reach the Host'); + assert.equal(transient.get('root')?.deliveryStatus, 'Waiting for earlier messages to be delivered', 'Main cannot reach the Host'); messages = messages.map((message) => ({ ...message, delivering: true })); await act(async () => changed('session-1')); assert.equal(transient.get('root')?.deliveryStatus, undefined, 'a message Main will deliver shows nothing'); assert.deepEqual(transient.get('root')?.deliveryActions, []); messages = messages.map((message) => ({ ...message, error: 'Saved locally. Waiting for the Host to become available.' })); await act(async () => changed('session-1')); - assert.equal(transient.get('root')?.deliveryStatus, 'Saved locally · waiting to send'); + assert.equal(transient.get('root')?.deliveryStatus, 'Waiting for earlier messages to be delivered'); assert.equal(transient.get('root')?.deliveryActions?.length, 1, 'a Host outage keeps the copy removable'); messages = messages.map((message) => ({ ...message, state: 'failed' })); await act(async () => changed('session-1')); diff --git a/apps/desktop/src/renderer/features/conversation/controller/session-local-messages.tsx b/apps/desktop/src/renderer/features/conversation/controller/session-local-messages.tsx index fb43e41780f..71fed0d5cc9 100644 --- a/apps/desktop/src/renderer/features/conversation/controller/session-local-messages.tsx +++ b/apps/desktop/src/renderer/features/conversation/controller/session-local-messages.tsx @@ -19,6 +19,7 @@ import { useEffect } from 'react'; import { useUiLocale, type TransientUserMessageProjection } from '@maka/ui'; +import { ICON_SIZE, Search, Trash2, X } from '@maka/ui/icons'; import { getSessionLocalCopy } from '../../../locales/session-local-copy.js'; import { useConversationServices } from '../services.js'; @@ -42,6 +43,7 @@ export function SessionLocalMessages(props: { .listMessages(sessionId) .then((messages) => { if (disposed || revision !== admitted) return; + let waitingForPrevious = false; for (const message of messages) { if (message.state === 'accepted' && !message.turnId) { // The Host queue owns accepted steering and follow-ups. A local @@ -49,12 +51,16 @@ export function SessionLocalMessages(props: { retire(sessionId, message.messageId); continue; } + const status = message.state === 'accepted' || message.state === 'sending' + || (message.state === 'saved' && message.delivering && !message.error) + ? undefined + : message.state === 'saved' && waitingForPrevious + ? copy.waitingForPrevious + : copy[message.state]; + if (message.state !== 'accepted' && message.state !== 'failed') waitingForPrevious = true; const action = (operation: () => Promise) => () => { void operation().catch(() => reportError(copy.updateError)); }; - const status = message.state === 'accepted' || message.state === 'sending' - || (message.state === 'saved' && message.delivering && !message.error) - ? undefined : copy[message.state]; publish(sessionId, { id: message.messageId, text: message.text, @@ -74,7 +80,10 @@ export function SessionLocalMessages(props: { : message.canCancel ? [ { - label: copy.remove, + label: message.state === 'failed' ? copy.remove : copy.cancel, + icon: message.state === 'failed' + ?