From adb6f41858113b894238e1076509a2b2d125926a Mon Sep 17 00:00:00 2001 From: Junmo Kim Date: Thu, 16 Jul 2026 13:33:51 +0900 Subject: [PATCH] fix(web): extend hover-reveal info affordance to touch devices (#1046) MessageInfoPopover's trigger button carried the same happy-message-actions-desktop-only class as the other hover-reveal actions from 4c76668a, so it never rendered on touch-only viewports (no hover: hover match). Switch it to the always-visible flex pattern the sibling copy button already uses, matching desktop's hover-reveal opacity animation on the parent row. Also widen the action row's desktop-only-row guard to stay reachable when a tool-only response (no copyable text) still carries model/ duration metadata from its first tool block, so the info popover isn't hidden behind an empty row on mobile. --- .../messages/MessageActions.test.tsx | 49 +++++++++++++++++++ .../AssistantChat/messages/MessageActions.tsx | 7 ++- web/src/index.css | 13 ----- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/web/src/components/AssistantChat/messages/MessageActions.test.tsx b/web/src/components/AssistantChat/messages/MessageActions.test.tsx index 89117c1d..e47f1d13 100644 --- a/web/src/components/AssistantChat/messages/MessageActions.test.tsx +++ b/web/src/components/AssistantChat/messages/MessageActions.test.tsx @@ -67,4 +67,53 @@ describe('MessageActions', () => { expect(screen.queryByRole('button', { name: 'Message details' })).toBeNull() }) + + it('keeps the info button reachable on touch devices (no hover-only class)', () => { + renderActions({ + align: 'start', + copyText: 'message body', + metadata: { durationMs: 1250, model: 'gpt-5.2-codex' } + }) + + const button = screen.getByRole('button', { name: 'Message details' }) + expect(button.className.split(' ')).not.toContain('happy-message-actions-desktop-only') + }) + + it('keeps the action row reachable on touch devices for tool-only messages with metadata', () => { + // Tool-only assistant turns (no trailing text) have no copyText, but + // can still carry model/duration metadata from the first tool block + // in the response group (see assistant-runtime.ts toThreadMessageLike). + renderActions({ + align: 'start', + copyText: undefined, + metadata: { durationMs: 1250, model: 'gpt-5.2-codex' } + }) + + const button = screen.getByRole('button', { name: 'Message details' }) + const row = button.closest('.happy-message-actions') + expect(row).not.toBeNull() + expect(row!.className.split(' ')).not.toContain('happy-message-actions-desktop-only-row') + }) + + it('keeps the timestamp reachable on touch devices (no hover-only class)', () => { + renderActions({ align: 'start', copyText: 'message body' }) + + const time = document.querySelector('time') + expect(time).not.toBeNull() + const wrapper = time!.parentElement! + expect(wrapper.className.split(' ')).not.toContain('happy-message-actions-desktop-only') + }) + + it('keeps the row reachable on touch devices even with neither copy text nor metadata (timestamp-only row)', () => { + // DesktopTimestamp always renders inside the row regardless of + // canCopy/hasMetadata, so the row is never actually empty -- hiding + // it via the hover-only row class would hide a real timestamp. + renderActions({ align: 'end', copyText: undefined, metadata: undefined }) + + const time = document.querySelector('time') + expect(time).not.toBeNull() + const row = time!.closest('.happy-message-actions') + expect(row).not.toBeNull() + expect(row!.className.split(' ')).not.toContain('happy-message-actions-desktop-only-row') + }) }) diff --git a/web/src/components/AssistantChat/messages/MessageActions.tsx b/web/src/components/AssistantChat/messages/MessageActions.tsx index 1238a4d4..9dc7f2d3 100644 --- a/web/src/components/AssistantChat/messages/MessageActions.tsx +++ b/web/src/components/AssistantChat/messages/MessageActions.tsx @@ -22,8 +22,7 @@ export function MessageActions({ align, copyText, metadata }: MessageActionsProp
{align === 'end' ? : null} @@ -46,7 +45,7 @@ export function MessageActions({ align, copyText, metadata }: MessageActionsProp function DesktopTimestamp() { return ( - + ) @@ -61,7 +60,7 @@ function MessageInfoPopover({ metadata }: { metadata: Omit diff --git a/web/src/index.css b/web/src/index.css index 612b0704..d515e443 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -433,11 +433,6 @@ body { margin-top: max(0px, calc((var(--app-chat-line-height) - var(--app-message-action-size)) / 2)); } -.happy-message-actions-desktop-only, -.happy-message-actions-desktop-only-row { - display: none; -} - @media (hover: hover) and (pointer: fine) { .happy-message-actions { opacity: 0; @@ -450,14 +445,6 @@ body { opacity: 1; pointer-events: auto; } - - .happy-message-actions-desktop-only { - display: inline-flex; - } - - .happy-message-actions-desktop-only-row { - display: flex; - } } .aui-md :where(.contains-task-list) {