fix(mobile): label the pill's running-state action as queue
While a turn runs, the collapsed pill showed a plain Send button for a draft that the expanded composer shows as Queue with a rotated icon, and the pill routed it through the primary action. The pill now queues, with the same label and icon as the expanded composer. Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
This commit is contained in:
@@ -3103,6 +3103,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
onExpand={mobileShell.expand}
|
||||
onApplySuggestion={applyAssistSuggestion}
|
||||
onPrimaryAction={handlePrimaryAction}
|
||||
onQueueMessage={() => { void handleQueueMessage(); }}
|
||||
onNewSession={handleMobileNewSession}
|
||||
onPickLocalFiles={handlePickLocalFiles}
|
||||
onOpenIssuePicker={openIssuePicker}
|
||||
|
||||
@@ -19,6 +19,7 @@ const renderPill = async (options: { hasContent: boolean; newSessionDraftOpen: b
|
||||
const container = document.createElement('div');
|
||||
const root = createRoot(container);
|
||||
let primaryActions = 0;
|
||||
let queued = 0;
|
||||
try {
|
||||
await act(async () => root.render(
|
||||
<SyncProvider directory="/fixture" sdk={createOpencodeClient({ baseUrl: "http://opencode.test", fetch: async () => new Response("[]", { headers: { "content-type": "application/json" } }) })}>
|
||||
@@ -40,6 +41,7 @@ const renderPill = async (options: { hasContent: boolean; newSessionDraftOpen: b
|
||||
onExpand={() => {}}
|
||||
onApplySuggestion={() => {}}
|
||||
onPrimaryAction={() => { primaryActions += 1; }}
|
||||
onQueueMessage={() => { queued += 1; }}
|
||||
onNewSession={() => {}}
|
||||
onPickLocalFiles={() => {}}
|
||||
onOpenIssuePicker={() => {}}
|
||||
@@ -51,11 +53,19 @@ const renderPill = async (options: { hasContent: boolean; newSessionDraftOpen: b
|
||||
</I18nProvider>
|
||||
</ThemeSystemProvider>
|
||||
</SyncProvider>));
|
||||
const send = container.querySelector<HTMLButtonElement>('[aria-label="Send message"]');
|
||||
if (options.hasContent) {
|
||||
if (options.hasContent && options.canAbort) {
|
||||
// While a turn runs the draft can only be queued, never sent past it.
|
||||
const queue = container.querySelector<HTMLButtonElement>('[aria-label="Queue message"]');
|
||||
expect(queue).not.toBeNull();
|
||||
await act(async () => { queue?.click(); });
|
||||
expect(queued).toBe(1);
|
||||
expect(primaryActions).toBe(0);
|
||||
} else if (options.hasContent) {
|
||||
const send = container.querySelector<HTMLButtonElement>('[aria-label="Send message"]');
|
||||
expect(send).not.toBeNull();
|
||||
await act(async () => { send?.click(); });
|
||||
expect(primaryActions).toBe(1);
|
||||
expect(queued).toBe(0);
|
||||
}
|
||||
return container.innerHTML;
|
||||
} finally {
|
||||
@@ -77,13 +87,17 @@ describe('MobilePillComposer', () => {
|
||||
expect(markup.indexOf('aria-label="Send message"')).toBeLessThan(markup.indexOf('aria-label="New chat"'));
|
||||
});
|
||||
|
||||
test('uses the trailing action to send content while the session is running', async () => {
|
||||
test('uses the trailing action to queue content while the session is running', async () => {
|
||||
// The expanded composer shows a rotated send icon labelled "Queue
|
||||
// message" in this state; the collapsed pill must read the same.
|
||||
const markup = await renderPill({ hasContent: true, newSessionDraftOpen: false, canAbort: true });
|
||||
|
||||
expect(markup).toContain('aria-label="Stop generating"');
|
||||
expect(markup).toContain('aria-label="Send message"');
|
||||
expect(markup).toContain('aria-label="Queue message"');
|
||||
expect(markup).toContain('-rotate-90');
|
||||
expect(markup).not.toContain('aria-label="Send message"');
|
||||
expect(markup).not.toContain('aria-label="New chat"');
|
||||
expect(markup.indexOf('aria-label="Stop generating"')).toBeLessThan(markup.indexOf('aria-label="Send message"'));
|
||||
expect(markup.indexOf('aria-label="Stop generating"')).toBeLessThan(markup.indexOf('aria-label="Queue message"'));
|
||||
});
|
||||
|
||||
test('uses the inline send action for content in a new-session draft', async () => {
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface MobilePillComposerProps {
|
||||
onExpand: () => void;
|
||||
onApplySuggestion: (text: string) => void;
|
||||
onPrimaryAction: () => void;
|
||||
/** While a turn runs, the trailing action queues, as the expanded composer does. */
|
||||
onQueueMessage: () => void;
|
||||
onNewSession: () => void;
|
||||
onPickLocalFiles: () => void;
|
||||
onOpenIssuePicker: () => void;
|
||||
@@ -66,6 +68,7 @@ export function MobilePillComposer(props: MobilePillComposerProps) {
|
||||
onExpand,
|
||||
onApplySuggestion,
|
||||
onPrimaryAction,
|
||||
onQueueMessage,
|
||||
onNewSession,
|
||||
onPickLocalFiles,
|
||||
onOpenIssuePicker,
|
||||
@@ -179,8 +182,10 @@ export function MobilePillComposer(props: MobilePillComposerProps) {
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{/* While running, Send moves outside because Abort owns the pill's
|
||||
end slot. An empty new-session draft needs neither action. */}
|
||||
{/* While running, Abort owns the pill's end slot and the outer button
|
||||
queues the draft, with the same rotated icon and label the expanded
|
||||
composer uses for that state. An empty new-session draft needs
|
||||
neither action. */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex-shrink-0 transition-all duration-200 ease-out',
|
||||
@@ -194,14 +199,14 @@ export function MobilePillComposer(props: MobilePillComposerProps) {
|
||||
showTrailingSendAction ? 'text-primary hover:text-primary' : 'text-foreground',
|
||||
)}
|
||||
style={{ backgroundColor: currentTheme?.colors?.surface?.subtle }}
|
||||
onClick={showTrailingSendAction ? onPrimaryAction : onNewSession}
|
||||
onClick={showTrailingSendAction ? onQueueMessage : onNewSession}
|
||||
disabled={newSessionDraftOpen && !showTrailingSendAction}
|
||||
title={t(showTrailingSendAction ? 'chat.chatInput.actions.sendMessageAria' : 'mobile.sessions.newChat')}
|
||||
aria-label={t(showTrailingSendAction ? 'chat.chatInput.actions.sendMessageAria' : 'mobile.sessions.newChat')}
|
||||
title={t(showTrailingSendAction ? 'chat.chatInput.actions.queueMessageAria' : 'mobile.sessions.newChat')}
|
||||
aria-label={t(showTrailingSendAction ? 'chat.chatInput.actions.queueMessageAria' : 'mobile.sessions.newChat')}
|
||||
>
|
||||
<Icon
|
||||
name={showTrailingSendAction ? 'send-plane-2' : 'add'}
|
||||
className={cn(showTrailingSendAction ? sendIconSizeClass : 'h-5 w-5', 'text-current')}
|
||||
className={cn(showTrailingSendAction ? cn(sendIconSizeClass, '-rotate-90') : 'h-5 w-5', 'text-current')}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user