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