You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/09/20 15:30:45 UTC

(maka) 04/09: fix(ui): flatten the composer staging area into one row-per-message list

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch fix/ui-queue-delivery-feedback
in repository https://gitbox.apache.org/repos/asf/maka.git

commit b18494050e9347255882fd4cba29704ec6f1592d
Author: AstroHan <le...@outlook.com>
AuthorDate: Sun Sep 20 20:59:36 2026 +0800

    fix(ui): flatten the composer staging area into one row-per-message list
    
    Queued steering, follow-ups and local in-flight sends are the same
    thing to the user — a message that has not landed yet — so the plate
    now renders one flat list instead of grouped sections and an outbox
    strip. Every row is a single line plus icon buttons: send-now, edit
    and delete for admitted entries; edit (cancel and restore the draft)
    and delete for unsent ones; a warning-tinted check-delivery for the
    unconfirmed state, which is the one case where "send now" would risk
    double execution. Group headers and persistent status text are gone;
    the send-shortcuts hint lives on a hover-revealed help icon.
    
    Generated with [Devin](https://devin.ai)
    
    Co-Authored-By: Devin <15...@users.noreply.github.com>
    Generated-by: Devin
---
 .../main/__tests__/message-queue-ui-state.test.ts  |  13 ++-
 apps/desktop/src/renderer/app-shell.tsx            |   9 +-
 .../contracts/transient-message-projection.ts      |   1 -
 .../controller/session-local-messages.tsx          |  54 +++++----
 .../src/renderer/locales/session-local-copy.ts     |   4 +
 apps/desktop/src/renderer/styles/composer.css      |  36 ++----
 .../stories/composer-message-queue.stories.tsx     |  10 +-
 .../ui/src/__tests__/composer-send-toggle.test.tsx |  28 ++---
 packages/ui/src/chat-view.tsx                      |   2 -
 packages/ui/src/composer-message-queue.tsx         | 123 ++++++---------------
 packages/ui/src/conversation-copy.ts               |  14 +--
 11 files changed, 115 insertions(+), 179 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 4382590b59..842a375c11 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
@@ -35,6 +35,7 @@ test('local delivery recovery cannot republish accepted Host queue rows', async
   let changed!: (sessionId: string) => void;
   const cancelled: string[][] = [];
   const reconciled: string[][] = [];
+  const restored: string[][] = [];
   let messages: DesktopLocalMessage[] = [
     { sessionId: 'session-1', messageId: 'steering', createdAt: 1, state: 'unknown', canCancel: false,
       text: 'steering', attachments: [], inlineReferences: [], placement: 'current_turn' },
@@ -59,23 +60,24 @@ test('local delivery recovery cannot republish accepted Host queue rows', async
       publish: (_id, message) => { transient.set(message.id, message); },
       retire: (_id, messageId) => { transient.delete(messageId); },
       reportError: (message) => { throw new Error(message); },
+      restoreDraft: (sessionId, text) => { restored.push([sessionId, text]); },
     }) }),
   })));
   const steering = transient.get('steering');
-  assert.equal(steering?.deliveryState, 'unconfirmed');
   assert.equal(steering?.deliveryStatus, 'Delivery unconfirmed. Do not send again.');
   assert.deepEqual(steering?.deliveryActions?.map((action) => action.label), ['Check delivery'],
     'an unconfirmed send offers only its receipt check, never cancellation');
   const followup = transient.get('followup');
-  assert.equal(followup?.deliveryState, 'pending');
   assert.equal(followup?.deliveryStatus, 'Waiting to send');
-  assert.deepEqual(followup?.deliveryActions?.map((action) => action.label), ['Cancel sending']);
+  assert.deepEqual(followup?.deliveryActions?.map((action) => action.label), ['Edit', 'Cancel sending']);
   await act(async () => { await steering?.deliveryActions?.[0]?.onClick(); });
   assert.deepEqual(reconciled, [['session-1', 'steering']]);
   assert.equal(transient.has('steering'), true, 'checking delivery does not retire the row');
   await act(async () => { await followup?.deliveryActions?.[0]?.onClick(); });
   assert.deepEqual(cancelled, [['session-1', 'followup']]);
-  assert.equal(transient.has('followup'), false, 'cancel retires the local row');
+  assert.deepEqual(restored, [['session-1', 'followup']],
+    'editing a never-dispatched message returns its text to the composer');
+  assert.equal(transient.has('followup'), false, 'edit retires the local row');
   messages = [
     { ...messages[0]!, state: 'failed', canCancel: true },
     { sessionId: 'session-1', messageId: 'settled', createdAt: 4, state: 'accepted', canCancel: false,
@@ -86,9 +88,8 @@ test('local delivery recovery cannot republish accepted Host queue rows', async
   ];
   await act(async () => changed('session-1'));
   const failed = transient.get('steering');
-  assert.equal(failed?.deliveryState, 'failed');
   assert.equal(failed?.deliveryStatus, 'Could not send · message kept');
-  assert.deepEqual(failed?.deliveryActions?.map((action) => action.label), ['Delete unsent message']);
+  assert.deepEqual(failed?.deliveryActions?.map((action) => action.label), ['Edit', 'Delete unsent message']);
   assert.deepEqual([...transient.keys()], ['steering', 'root', 'later']);
   assert.equal(transient.get('root')?.transientPlacement, 'current_turn');
   await act(async () => changed('session-1'));
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index 389e422325..c52ce7d4cc 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -608,6 +608,13 @@ function AppShellContent({
   const openComposerModelPicker = useCallback(() => {
     composerRef.current?.openModelPicker();
   }, []);
+  const restoreLocalMessageDraft = useCallback((sessionId: string, text: string) => {
+    const composer = composerRef.current;
+    if (!composer || activeIdRef.current !== sessionId) return;
+    if (composer.getText().trim()) composer.appendText(text);
+    else composer.setText(text);
+    composer.focus();
+  }, [activeIdRef]);
   const retractedWorkspaceReferencesRef = useRef<Record<string, InlineReference[]>>({});
   const [revisionDraft, setRevisionDraft] = useState<TurnRevisionDraft | null>(null);
   const revisionDraftRef = useRef<TurnRevisionDraft | null>(null);
@@ -2196,7 +2203,7 @@ function AppShellContent({
       canOpenDialog={activeBoundarySurface.localInteractionAvailable}
       reportError={showSessionError}
     >
-    <Conversation.SessionLocalMessages sessionId={activeId} publish={addTransientMessage} retire={removeTransientMessage} reportError={toastApi.error} />
+    <Conversation.SessionLocalMessages sessionId={activeId} publish={addTransientMessage} retire={removeTransientMessage} reportError={toastApi.error} restoreDraft={restoreLocalMessageDraft} />
     <ModuleHub.ModuleHubProvider
       selection={navSelection}
       selectModule={setNavSelection}
diff --git a/apps/desktop/src/renderer/application/contracts/transient-message-projection.ts b/apps/desktop/src/renderer/application/contracts/transient-message-projection.ts
index 7e8f98b61b..4b89d962ed 100644
--- a/apps/desktop/src/renderer/application/contracts/transient-message-projection.ts
+++ b/apps/desktop/src/renderer/application/contracts/transient-message-projection.ts
@@ -52,7 +52,6 @@ export function mergeTransientMessageProjection(
     // A Message's send time is written once; an update's `ts` must not move it.
     ts: current.ts,
     ...(update.pendingSteering === undefined && current.pendingSteering !== undefined ? { pendingSteering: current.pendingSteering } : {}),
-    ...(!Object.hasOwn(update, 'deliveryState') && current.deliveryState !== undefined ? { deliveryState: current.deliveryState } : {}),
     ...(!Object.hasOwn(update, 'deliveryStatus') && current.deliveryStatus !== undefined ? { deliveryStatus: current.deliveryStatus } : {}),
     ...(!Object.hasOwn(update, 'deliveryDetail') && current.deliveryDetail !== undefined ? { deliveryDetail: current.deliveryDetail } : {}),
     ...(!Object.hasOwn(update, 'deliveryActions') && current.deliveryActions !== undefined ? { deliveryActions: current.deliveryActions } : {}),
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 4d293dfaed..22ee720b47 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,7 +19,7 @@
 
 import { useEffect } from 'react';
 import { useUiLocale, type TransientUserMessageProjection } from '@maka/ui';
-import { ICON_SIZE, Search, Trash2, X } from '@maka/ui/icons';
+import { ICON_SIZE, Pencil, Search, Trash2 } from '@maka/ui/icons';
 import { getSessionLocalCopy } from '../../../locales/session-local-copy.js';
 import { useConversationServices } from '../services.js';
 
@@ -28,10 +28,12 @@ export function SessionLocalMessages(props: {
   readonly publish: (sessionId: string, message: TransientUserMessageProjection) => void;
   readonly retire: (sessionId: string, messageId: string) => void;
   readonly reportError: (message: string) => void;
+  /** Puts a never-dispatched message's text back into the composer for editing. */
+  readonly restoreDraft?: (sessionId: string, text: string) => void;
 }): null {
   const services = useConversationServices();
   const locale = useUiLocale();
-  const { sessionId, publish, retire, reportError } = props;
+  const { sessionId, publish, retire, reportError, restoreDraft } = props;
   useEffect(() => {
     if (!sessionId) return;
     let disposed = false;
@@ -62,34 +64,40 @@ export function SessionLocalMessages(props: {
               quotes: message.quotes,
               inlineReferences: message.inlineReferences,
               hostTurnId: message.turnId,
-              deliveryState: message.state === 'unknown'
-                ? 'unconfirmed'
-                : message.state === 'failed'
-                  ? 'failed'
-                  : 'pending',
               deliveryStatus: copy[message.state],
               deliveryDetail: message.error,
-              deliveryActions: message.canCancel
+              deliveryActions: message.state === 'unknown'
                 ? [
                     {
-                      label: message.state === 'failed' ? copy.remove : copy.cancel,
-                      icon: message.state === 'failed'
-                        ? <Trash2 size={ICON_SIZE.control} aria-hidden="true" />
-                        : <X size={ICON_SIZE.control} aria-hidden="true" />,
-                      onClick: action(async () => {
-                        await services.cancelMessage(sessionId, message.messageId);
-                        retire(sessionId, message.messageId);
-                      }),
+                      label: copy.check,
+                      icon: <Search size={ICON_SIZE.control} style={{ color: 'var(--warning-text)' }} aria-hidden="true" />,
+                      onClick: action(() =>
+                        services.reconcileMessage(sessionId, message.messageId),
+                      ),
                     },
                   ]
-                : message.state === 'unknown'
+                : message.canCancel
                   ? [
+                      ...(restoreDraft
+                        ? [{
+                            label: copy.edit,
+                            icon: <Pencil size={ICON_SIZE.control} aria-hidden="true" />,
+                            onClick: action(async () => {
+                              await services.cancelMessage(sessionId, message.messageId);
+                              retire(sessionId, message.messageId);
+                              restoreDraft(sessionId, message.text);
+                            }),
+                          }]
+                        : []),
                       {
-                        label: copy.check,
-                        icon: <Search size={ICON_SIZE.control} aria-hidden="true" />,
-                        onClick: action(() =>
-                          services.reconcileMessage(sessionId, message.messageId),
-                        ),
+                        label: message.state === 'failed' ? copy.remove : copy.cancel,
+                        icon: message.state === 'failed'
+                          ? <Trash2 size={ICON_SIZE.control} style={{ color: 'var(--destructive-text)' }} aria-hidden="true" />
+                          : <Trash2 size={ICON_SIZE.control} aria-hidden="true" />,
+                        onClick: action(async () => {
+                          await services.cancelMessage(sessionId, message.messageId);
+                          retire(sessionId, message.messageId);
+                        }),
                       },
                     ]
                   : [],
@@ -106,6 +114,6 @@ export function SessionLocalMessages(props: {
       disposed = true;
       unsubscribe();
     };
-  }, [sessionId, services, publish, retire, reportError, locale]);
+  }, [sessionId, services, publish, retire, reportError, restoreDraft, locale]);
   return null;
 }
diff --git a/apps/desktop/src/renderer/locales/session-local-copy.ts b/apps/desktop/src/renderer/locales/session-local-copy.ts
index d29b4080ad..1311b83de1 100644
--- a/apps/desktop/src/renderer/locales/session-local-copy.ts
+++ b/apps/desktop/src/renderer/locales/session-local-copy.ts
@@ -25,6 +25,7 @@ interface SessionLocalCopy {
   accepted: string;
   unknown: string;
   failed: string;
+  edit: string;
   remove: string;
   cancel: string;
   check: string;
@@ -38,6 +39,7 @@ const catalog = {
     accepted: 'Delivered',
     unknown: 'Delivery unconfirmed. Do not send again.',
     failed: 'Could not send · message kept',
+    edit: 'Edit',
     remove: 'Delete unsent message',
     cancel: 'Cancel sending',
     check: 'Check delivery',
@@ -49,6 +51,7 @@ const catalog = {
     accepted: '已送达',
     unknown: '暂时无法确认是否送达,请勿重复发送',
     failed: '未能发送 · 消息已保留',
+    edit: '编辑',
     remove: '删除未发送的消息',
     cancel: '取消发送',
     check: '检查是否送达',
@@ -60,6 +63,7 @@ const catalog = {
     accepted: '已送達',
     unknown: '暫時無法確認是否送達,請勿重複傳送',
     failed: '無法傳送 · 訊息已保留',
+    edit: '編輯',
     remove: '刪除未傳送的訊息',
     cancel: '取消傳送',
     check: '檢查是否送達',
diff --git a/apps/desktop/src/renderer/styles/composer.css b/apps/desktop/src/renderer/styles/composer.css
index 56d1038991..85fa9d7707 100644
--- a/apps/desktop/src/renderer/styles/composer.css
+++ b/apps/desktop/src/renderer/styles/composer.css
@@ -62,6 +62,7 @@
 }
 
 .maka-composer-queue {
+  position: relative;
   width: min(var(--maka-reading-measure), calc(100% - (2 * var(--space-6))));
   max-width: var(--maka-reading-measure);
   margin: 0 auto var(--space-1);
@@ -129,24 +130,17 @@
   cursor: grabbing;
 }
 
-.maka-composer-queue-local:not(:first-child) {
-  border-top: var(--border-width-hairline) solid var(--border);
-}
-
-.maka-composer-queue-delivery {
-  display: grid;
-  place-items: center;
-  width: 16px;
-  color: var(--muted-foreground);
-  outline: none;
-}
-
-.maka-composer-queue-delivery[data-tone='unconfirmed'] {
-  color: var(--warning-text);
+.maka-composer-queue-help {
+  position: absolute;
+  top: var(--space-1);
+  right: var(--space-1);
+  opacity: 0;
+  transition: opacity 120ms ease;
 }
 
-.maka-composer-queue-delivery[data-tone='failed'] {
-  color: var(--destructive-text);
+.maka-composer-queue:hover .maka-composer-queue-help,
+.maka-composer-queue-help:focus-visible {
+  opacity: 1;
 }
 
 @media (max-width: 640px) {
@@ -692,13 +686,3 @@
   gap: var(--space-3);
 }
 
-.maka-composer-queue-status {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: var(--space-2);
-  padding: var(--space-1) var(--space-2);
-  color: var(--muted-foreground);
-  font: var(--maka-text-body);
-  white-space: nowrap;
-}
diff --git a/apps/desktop/stories/composer-message-queue.stories.tsx b/apps/desktop/stories/composer-message-queue.stories.tsx
index a49196d731..cf39af06f2 100644
--- a/apps/desktop/stories/composer-message-queue.stories.tsx
+++ b/apps/desktop/stories/composer-message-queue.stories.tsx
@@ -17,13 +17,13 @@
  * under the License.
  */
 
-import { useCallback, useMemo, useState } from 'react';
+import { useCallback, useMemo, useRef, useState } from 'react';
 import type { Meta, StoryObj } from '@storybook/react-vite';
 import type { ComponentProps } from 'react';
 import type { MessageQueueEntryProjection } from '@maka/core/events';
 import type { SessionSummary } from '@maka/core/session';
 import { Composer } from '@maka/ui';
-import type { ChatModelChoice, TransientUserMessageProjection } from '@maka/ui';
+import type { ChatModelChoice, ComposerHandle, TransientUserMessageProjection } from '@maka/ui';
 import {
   ConversationServicesProvider,
   SessionLocalMessages,
@@ -175,6 +175,7 @@ function followUpEntry(entryId: string, text: string): MessageQueueEntryProjecti
  * out) is the real one; only the authority is simulated.
  */
 function QueuedComposer({ deliveryState }: { deliveryState: DeliveryState }) {
+  const composerRef = useRef<ComposerHandle>(null);
   const [followup, setFollowup] = useState<MessageQueueEntryProjection[]>([
     {
       entryId: 'entry-steer',
@@ -223,9 +224,14 @@ function QueuedComposer({ deliveryState }: { deliveryState: DeliveryState }) {
         publish={publish}
         retire={retire}
         reportError={noop}
+        restoreDraft={(_id, text) => {
+          composerRef.current?.setText(text);
+          composerRef.current?.focus();
+        }}
       />
       <Composer
         {...base}
+        ref={composerRef}
         queuedMessages={deliveryState === 'queued' ? followup : []}
         pendingMessages={[...published.values()]}
         queuedMessageRevision={1}
diff --git a/packages/ui/src/__tests__/composer-send-toggle.test.tsx b/packages/ui/src/__tests__/composer-send-toggle.test.tsx
index 4e96ab9cc1..12ebbc927a 100644
--- a/packages/ui/src/__tests__/composer-send-toggle.test.tsx
+++ b/packages/ui/src/__tests__/composer-send-toggle.test.tsx
@@ -301,7 +301,7 @@ test('keeps Host order visible until the reordered projection arrives', async ()
     const deleteButtons = [
       ...container.querySelectorAll<HTMLButtonElement>('[aria-label="Delete"]'),
     ];
-    assert.equal(container.querySelectorAll('[aria-label="Steer"]').length, 2);
+    assert.equal(container.querySelectorAll('[aria-label="Send now"]').length, 2);
     assert.equal(editButtons.length, 3);
     assert.equal(deleteButtons.length, 3);
     await act(async () => {
@@ -375,16 +375,11 @@ test('deduplicates pending steering against Host queue entries and keeps the pla
     const document = parseHTML(`<html><body>${markup}</body></html>`).document;
     assert.equal(document.querySelectorAll('.maka-composer-queue-text').length, 1);
     assert.equal(document.querySelector('.maka-composer-queue-text')?.textContent, pending.text);
-    assert.equal(
-      document.querySelector('.maka-composer-queue-status')?.textContent ?? null,
-      entries.length > 0 ? 'Steering' : null,
-      'only Host-admitted entries carry a queue group; a pending send stays in the outbox',
-    );
   }
 });
 
 
-test('local sends render as an outbox, not as admitted queue entries', () => {
+test('local sends share the flat staging list with icon-only delivery actions', () => {
   const markup = renderToStaticMarkup(<LocaleProvider locale="en"><Composer onSend={() => undefined} onStop={() => undefined}
     queuedMessages={[{
       entryId: 'admitted', messageId: 'message-admitted',
@@ -392,25 +387,22 @@ test('local sends render as an outbox, not as admitted queue entries', () => {
     }]}
     pendingMessages={[
       { id: 'local', text: 'offline follow-up', ts: 1, transientPlacement: 'next_turn',
-        deliveryState: 'unconfirmed', deliveryStatus: 'Delivery uncertain', deliveryDetail: 'Connection interrupted',
+        deliveryStatus: 'Delivery uncertain', deliveryDetail: 'Connection interrupted',
         deliveryActions: [{ label: 'Check delivery', icon: <Search size={ICON_SIZE.control} aria-hidden="true" />, onClick() {} }] },
       { id: 'sending', text: 'still sending', ts: 2, transientPlacement: 'next_turn',
-        deliveryState: 'pending', deliveryStatus: 'Sending…' },
+        deliveryStatus: 'Sending…' },
     ]} /></LocaleProvider>);
   const document = parseHTML(`<html><body>${markup}</body></html>`).document;
   assert.deepEqual(
-    [...document.querySelectorAll('.maka-composer-queue-local li')].map((row) => row.textContent),
-    ['offline follow-up', 'still sending'],
-  );
-  assert.deepEqual(
-    [...document.querySelectorAll('[data-queue-placement="next_turn"] li')].map((row) => row.textContent),
-    ['admitted follow-up'],
-    'an unconfirmed send never joins the Host-admitted follow-up group',
+    [...document.querySelectorAll('.maka-composer-queue-list li')].map((row) => row.textContent),
+    ['admitted follow-up', 'offline follow-up', 'still sending'],
+    'admitted entries and in-flight sends share one flat list',
   );
-  const delivery = document.querySelector('.maka-composer-queue-delivery[data-tone="unconfirmed"]');
-  assert.equal(delivery?.getAttribute('aria-label'), 'Delivery uncertain');
   assert.ok(document.querySelector('.maka-composer-queue-actions button[aria-label="Check delivery"]'),
     'the delivery action stays an accessible labelled icon control');
+  const gripRows = [...document.querySelectorAll('li')]
+    .filter((row) => row.querySelector('.maka-composer-queue-grip') !== null);
+  assert.equal(gripRows.length, 1, 'only the admitted row carries a drag grip');
   const sendingRow = [...document.querySelectorAll('li')]
     .find((row) => row.textContent?.includes('still sending'));
   assert.ok(sendingRow);
diff --git a/packages/ui/src/chat-view.tsx b/packages/ui/src/chat-view.tsx
index 1ed7f4780e..400a59ceea 100644
--- a/packages/ui/src/chat-view.tsx
+++ b/packages/ui/src/chat-view.tsx
@@ -148,8 +148,6 @@ export interface ChatViewGoalIndicatorProps {
 export interface TransientUserMessageProjection {
   /** Held above the composer until Runtime emits steering_message. */
   pendingSteering?: boolean;
-  /** Outbox tone for the composer row: pending sends differ from ones needing attention. */
-  deliveryState?: 'pending' | 'unconfirmed' | 'failed';
   deliveryStatus?: string;
   deliveryDetail?: string;
   deliveryActions?: readonly { label: string; icon: ReactNode; onClick(): void | Promise<void> }[];
diff --git a/packages/ui/src/composer-message-queue.tsx b/packages/ui/src/composer-message-queue.tsx
index 410289fe17..f4b63e03bb 100644
--- a/packages/ui/src/composer-message-queue.tsx
+++ b/packages/ui/src/composer-message-queue.tsx
@@ -23,7 +23,7 @@ import type { MessageQueueEntryProjection } from '@maka/core/events';
 import { IconButton, Tooltip } from '@astryxdesign/core';
 import { List, ListItem } from '@astryxdesign/core/List';
 import type { ConversationCopy } from './conversation-copy.js';
-import { AlertCircle, AlertTriangle, Check, CornerDownLeft, GripVertical, HelpCircle, Hourglass, ICON_SIZE, Pencil, Trash2, X } from './icons.js';
+import { Check, CornerDownLeft, GripVertical, HelpCircle, ICON_SIZE, Pencil, Trash2, X } from './icons.js';
 import { useMountedRef } from './use-mounted-ref.js';
 
 type ComposerQueueEntry = Omit<MessageQueueEntryProjection, 'state'> & {
@@ -73,14 +73,6 @@ export const ComposerMessageQueue = memo(function ComposerMessageQueue(
   const copy = props.copy;
 
   const entries = props.queuedMessages;
-  // The groups hold Host-admitted entries only. A local message still waiting
-  // for its receipt is an outgoing send, not a queued item — mixing it into
-  // "next turn" claims an admission the Host never gave.
-  const localEntries = entries.filter((entry) => entry.localMessage !== undefined);
-  const groups = (['current_turn', 'next_turn'] as const).map((placement) => ({
-    placement,
-    entries: entries.filter((entry) => entry.placement === placement && entry.localMessage === undefined),
-  })).filter((group) => group.entries.length > 0);
 
   async function runEntryAction(
     entryId: string,
@@ -152,25 +144,25 @@ export const ComposerMessageQueue = memo(function ComposerMessageQueue(
       role="region"
       aria-label={copy.queuedMessagesAriaLabel(entries.length)}
     >
-      {groups.map((group, index) => <section key={group.placement} data-queue-placement={group.placement}>
-        <div className="maka-composer-queue-status">
-          <span>{group.placement === 'current_turn' ? copy.steeringPending : copy.followupPending}</span>
-          {index === 0 && <Tooltip alignment="end" content={<span style={{ whiteSpace: 'pre-line' }}>{copy.queueShortcuts}</span>}>
-            <IconButton variant="ghost" size="sm" type="button" label={copy.queueShortcutsLabel}
-              icon={<HelpCircle size={ICON_SIZE.control} aria-hidden="true" />} />
-          </Tooltip>}
-        </div>
-        <List className="maka-composer-queue-list" density="compact">
-        {group.entries.map((entry) => {
+      <Tooltip alignment="end" content={<span style={{ whiteSpace: 'pre-line' }}>{copy.queueShortcuts}</span>}>
+        <IconButton className="maka-composer-queue-help" variant="ghost" size="sm" type="button"
+          label={copy.queueShortcutsLabel}
+          icon={<HelpCircle size={ICON_SIZE.control} aria-hidden="true" />} />
+      </Tooltip>
+      <List className="maka-composer-queue-list" density="compact">
+        {entries.map((entry) => {
           const editing = editingEntryId === entry.entryId;
+          const local = entry.localMessage;
           const reorderable =
             entry.state === 'queued'
+            && !local
             && !editing
             && Boolean(props.onReorderEntries)
             && pendingEntryId === null;
           return (
             <div
               key={entry.entryId}
+              data-queue-placement={entry.placement}
               data-maka-queue-drop-target={reorderable ? 'true' : undefined}
               onDragOver={(event) => {
                 if (reorderable && dragEntryId.current) event.preventDefault();
@@ -206,7 +198,7 @@ export const ComposerMessageQueue = memo(function ComposerMessageQueue(
                 </span>
               )}
               style={{ minHeight: 28, paddingBlock: 0 }}
-              startContent={(
+              startContent={local ? undefined : (
                 <span
                   className="maka-composer-queue-grip"
                   draggable={reorderable}
@@ -226,7 +218,12 @@ export const ComposerMessageQueue = memo(function ComposerMessageQueue(
               )}
               endContent={(
                 <span className="maka-composer-queue-actions">
-                  {editing ? (
+                  {local ? local.deliveryActions?.map((action) => (
+                    <IconButton key={action.label} variant="ghost" size="sm" type="button"
+                      label={action.label}
+                      tooltip={local.deliveryStatus ? `${local.deliveryStatus} · ${action.label}` : action.label}
+                      icon={action.icon} clickAction={action.onClick} />
+                  )) : editing ? (
                     <>
                       <IconButton
                         variant="ghost"
@@ -251,21 +248,6 @@ export const ComposerMessageQueue = memo(function ComposerMessageQueue(
                     </>
                   ) : (
                     <>
-                      <IconButton
-                        variant="ghost"
-                        size="sm"
-                        type="button"
-                        isDisabled={
-                          pendingEntryId !== null
-                          || entry.state !== 'queued'
-                          || props.queueRevision === undefined
-                          || !props.onUpdateEntry
-                        }
-                        label={copy.editQueuedEntry}
-                        tooltip={copy.editQueuedEntry}
-                        onClick={() => beginEdit(entry)}
-                        icon={<Pencil size={ICON_SIZE.control} aria-hidden="true" />}
-                      />
                       {entry.placement === 'next_turn' ? (
                         <IconButton
                           variant="ghost"
@@ -283,6 +265,21 @@ export const ComposerMessageQueue = memo(function ComposerMessageQueue(
                           icon={<CornerDownLeft size={ICON_SIZE.control} aria-hidden="true" />}
                         />
                       ) : null}
+                      <IconButton
+                        variant="ghost"
+                        size="sm"
+                        type="button"
+                        isDisabled={
+                          pendingEntryId !== null
+                          || entry.state !== 'queued'
+                          || props.queueRevision === undefined
+                          || !props.onUpdateEntry
+                        }
+                        label={copy.editQueuedEntry}
+                        tooltip={copy.editQueuedEntry}
+                        onClick={() => beginEdit(entry)}
+                        icon={<Pencil size={ICON_SIZE.control} aria-hidden="true" />}
+                      />
                       <IconButton
                         variant="ghost"
                         size="sm"
@@ -306,59 +303,7 @@ export const ComposerMessageQueue = memo(function ComposerMessageQueue(
             </div>
           );
         })}
-        </List>
-      </section>)}
-      {localEntries.length > 0 && (
-        <section className="maka-composer-queue-local">
-          <List className="maka-composer-queue-list" density="compact">
-            {localEntries.map((entry) => {
-              const local = entry.localMessage;
-              const tone = local?.deliveryState ?? 'pending';
-              const text = entry.content.displayText ?? entry.content.text;
-              return (
-                <ListItem
-                  key={entry.entryId}
-                  style={{ minHeight: 28, paddingBlock: 0 }}
-                  label={(
-                    <span className="maka-composer-queue-text" title={text}>
-                      {text}
-                    </span>
-                  )}
-                  startContent={local?.deliveryStatus ? (
-                    <Tooltip alignment="end" content={local.deliveryStatus} focusTrigger="always" touchTrigger="tap">
-                      <span
-                        className="maka-composer-queue-delivery"
-                        data-tone={tone}
-                        role="img"
-                        tabIndex={0}
-                        aria-label={local.deliveryStatus}
-                      >
-                        {tone === 'failed' ? (
-                          <AlertCircle size={ICON_SIZE.control} aria-hidden="true" />
-                        ) : tone === 'unconfirmed' ? (
-                          <AlertTriangle size={ICON_SIZE.control} aria-hidden="true" />
-                        ) : (
-                          <Hourglass size={ICON_SIZE.control} aria-hidden="true" />
-                        )}
-                      </span>
-                    </Tooltip>
-                  ) : undefined}
-                  endContent={(
-                    <span className="maka-composer-queue-actions">
-                      {local?.deliveryActions?.map((action) => (
-                        <IconButton key={action.label} variant="ghost" size="sm" type="button"
-                          label={action.label}
-                          tooltip={local.deliveryStatus ? `${local.deliveryStatus} · ${action.label}` : action.label}
-                          icon={action.icon} clickAction={action.onClick} />
-                      ))}
-                    </span>
-                  )}
-                />
-              );
-            })}
-          </List>
-        </section>
-      )}
+      </List>
     </div>
   );
 });
diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts
index 45b36731e5..06c955dcdc 100644
--- a/packages/ui/src/conversation-copy.ts
+++ b/packages/ui/src/conversation-copy.ts
@@ -129,8 +129,6 @@ export interface ConversationCopy {
     importing: string;
     sendLabel: string;
     queuedMessagesAriaLabel(count: number): string;
-    steeringPending: string;
-    followupPending: string;
     queueShortcutsLabel: string;
     queueShortcuts: string;
     promoteQueuedEntry: string;
@@ -513,11 +511,9 @@ const CONVERSATION_COPY = {
       placeholder: '描述任务,@ 引用文件或会话,/ 选择技能…', textareaAriaLabel: '消息输入框', pastedQuoteLabel: '粘贴的文本', selectedSkillsAriaLabel: '已选择的 Skill', removeSkillAriaLabel: (name) => `移除 Skill:${name}`, awaitingPermission: '等待你确认权限…',
       sending: '正在发送…', importing: '正在导入…', sendLabel: '发送',
       queuedMessagesAriaLabel: (count) => `${count} 条待发送消息`,
-      steeringPending: '调整方向',
-      followupPending: '下一轮',
       queueShortcutsLabel: '发送快捷键',
       queueShortcuts: 'Shift+Enter:调整当前轮方向,待处理消息一起生效。\nEnter:排到下一轮,每轮处理一条。',
-      promoteQueuedEntry: '调整方向', editQueuedEntry: '编辑', saveQueuedEntry: '保存', cancelQueuedEntryEdit: '取消编辑', deleteQueuedEntry: '删除', reorderQueuedEntry: '拖动排序',
+      promoteQueuedEntry: '直接发送', editQueuedEntry: '编辑', saveQueuedEntry: '保存', cancelQueuedEntryEdit: '取消编辑', deleteQueuedEntry: '删除', reorderQueuedEntry: '拖动排序',
       stopLabel: '停止', stopping: '停止中…',
       streaming: 'Maka 正在回答…', processing: 'Maka 正在处理…', continuing: 'Maka 继续中…',
       interruptHint: '或点停止中断', addContext: '添加上下文', stagedContext: '附加内容',
@@ -674,11 +670,9 @@ const CONVERSATION_COPY = {
       placeholder: '描述任務,@ 引用檔案,/ 選擇技能…', textareaAriaLabel: '訊息輸入框', pastedQuoteLabel: '貼上的文本', selectedSkillsAriaLabel: '已選擇的 Skill', removeSkillAriaLabel: (name) => `移除 Skill:${name}`, awaitingPermission: '等待你確認權限…',
       sending: '正在傳送…', importing: '正在匯入…', sendLabel: '傳送',
       queuedMessagesAriaLabel: (count) => `${count} 條待發送訊息`,
-      steeringPending: '調整方向',
-      followupPending: '下一輪',
       queueShortcutsLabel: '傳送快速鍵',
       queueShortcuts: 'Shift+Enter:調整目前這輪方向,待處理訊息一起生效。\nEnter:排到下一輪,每輪處理一則。',
-      promoteQueuedEntry: '調整方向', editQueuedEntry: '編輯', saveQueuedEntry: '儲存', cancelQueuedEntryEdit: '取消編輯', deleteQueuedEntry: '刪除', reorderQueuedEntry: '拖動排序',
+      promoteQueuedEntry: '直接傳送', editQueuedEntry: '編輯', saveQueuedEntry: '儲存', cancelQueuedEntryEdit: '取消編輯', deleteQueuedEntry: '刪除', reorderQueuedEntry: '拖動排序',
       stopLabel: '停止', stopping: '停止中…',
       streaming: 'Maka 正在回答…', processing: 'Maka 正在處理…', continuing: 'Maka 繼續中…',
       interruptHint: '或點停止中斷', addContext: '新增上下文', stagedContext: '附加內容',
@@ -864,11 +858,9 @@ const CONVERSATION_COPY = {
       placeholder: 'Describe a task, @ to reference files or sessions, / for skills…', textareaAriaLabel: 'Message input', pastedQuoteLabel: 'Pasted text', selectedSkillsAriaLabel: 'Selected Skills', removeSkillAriaLabel: (name) => `Remove Skill: ${name}`, awaitingPermission: 'Waiting for your permission decision…',
       sending: 'Sending…', importing: 'Importing…', sendLabel: 'Send',
       queuedMessagesAriaLabel: (count) => `${count} queued message${count === 1 ? '' : 's'}`,
-      steeringPending: 'Steering',
-      followupPending: 'Next turn',
       queueShortcutsLabel: 'Send shortcuts',
       queueShortcuts: 'Shift+Enter: Steer the current turn; pending messages are applied together.\nEnter: Queue for the next turn; one message per turn.',
-      promoteQueuedEntry: 'Steer', editQueuedEntry: 'Edit', saveQueuedEntry: 'Save', cancelQueuedEntryEdit: 'Cancel editing', deleteQueuedEntry: 'Delete', reorderQueuedEntry: 'Drag to reorder',
+      promoteQueuedEntry: 'Send now', editQueuedEntry: 'Edit', saveQueuedEntry: 'Save', cancelQueuedEntryEdit: 'Cancel editing', deleteQueuedEntry: 'Delete', reorderQueuedEntry: 'Drag to reorder',
       stopLabel: 'Stop', stopping: 'Stopping…',
       streaming: 'Maka is responding…', processing: 'Maka is working…', continuing: 'Maka is continuing…',
       interruptHint: 'or click Stop to interrupt', addContext: 'Add context', stagedContext: 'staged items',


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org