diff --git a/packages/components/src/components/ai-gui/view.tsx b/packages/components/src/components/ai-gui/view.tsx index b6cbd832c..38f2e7fa7 100644 --- a/packages/components/src/components/ai-gui/view.tsx +++ b/packages/components/src/components/ai-gui/view.tsx @@ -3629,7 +3629,7 @@ const AssistantForkButton = ({ ); }; -const AssistantTurnFooter = ({ +export const AssistantTurnFooter = ({ message, sessionId, fileDiffOverride, @@ -3737,7 +3737,7 @@ const AssistantTurnFooter = ({ 'flex flex-wrap items-center justify-start text-[11px] text-muted-foreground', isMobile ? 'min-h-6 gap-1' : 'min-h-7 gap-2', !isMobile && 'opacity-0 transition-opacity duration-150 focus-within:opacity-100', - !isMobile && isTurnHovered && 'opacity-100' + !isMobile && (isTurnHovered || isForking) && 'opacity-100' )} data-assistant-turn-actions > diff --git a/packages/components/src/stories/AssistantTurnAlignment.stories.tsx b/packages/components/src/stories/AssistantTurnAlignment.stories.tsx index 042b75b39..688f61b9f 100644 --- a/packages/components/src/stories/AssistantTurnAlignment.stories.tsx +++ b/packages/components/src/stories/AssistantTurnAlignment.stories.tsx @@ -177,6 +177,24 @@ export const DesktopFinishedTurn: Story = { ), }; +export const DesktopForkingTurn: Story = { + args: { sessionId, items: finishedItems, renderMessageRow }, + globals: { theme: 'dark' }, + render: () => ( +
+ undefined} + forkingAssistantMessageId={finishedTurn.id} + /> +
+ ), +}; + /** * PLAN-MODE TURN — the widest set of top-level row shells in one column. * diff --git a/packages/components/tests/assistant-turn-action-inset.test.ts b/packages/components/tests/assistant-turn-action-inset.test.ts index 930485c15..f4084b2e6 100644 --- a/packages/components/tests/assistant-turn-action-inset.test.ts +++ b/packages/components/tests/assistant-turn-action-inset.test.ts @@ -1,7 +1,21 @@ +// @vitest-environment jsdom + +import { act, createElement } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import type { SessionHistoryParsed, SessionId } from '@lody/shared'; import { describe, expect, it } from 'vitest'; -import { MOBILE_TURN_ACTION_LEADING_INSET_PX } from '../src/components/ai-gui/view'; +import { + AssistantTurnFooter, + MOBILE_TURN_ACTION_LEADING_INSET_PX, +} from '../src/components/ai-gui/view'; import { EDGE_ZONE_PX } from '../src/components/mobile/mobile-edge-back-swipe'; +import { ForceDesktopLayoutProvider } from '../src/hooks/use-mobile'; +import { initI18n } from '../src/i18n'; + +( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } +).IS_REACT_ACT_ENVIRONMENT = true; /* * The mobile assistant-turn action bar puts the turn duration in front of the @@ -29,3 +43,45 @@ describe('mobile assistant-turn action bar inset', () => { expect(EDGE_ZONE_PX).toBeGreaterThan(0); }); }); + +describe('desktop assistant-turn action bar visibility', () => { + it('keeps the whole action group visible while a fork is pending', async () => { + await initI18n('en'); + const container = document.createElement('div'); + document.body.appendChild(container); + const root: Root = createRoot(container); + const message = { + id: 'assistant-turn-forking', + role: 'assistant', + timestamp: '2026-09-10T03:00:00.000Z', + endedAt: '2026-09-10T03:00:01.000Z', + finished: true, + items: [{ type: 'text', text: 'A completed response.' }], + } as unknown as SessionHistoryParsed; + + await act(async () => { + root.render( + createElement( + ForceDesktopLayoutProvider, + null, + createElement(AssistantTurnFooter, { + message, + sessionId: 'session-forking' as SessionId, + showDuration: true, + isTurnHovered: false, + onFork: () => undefined, + isForking: true, + }) + ) + ); + }); + + const actions = container.querySelector('[data-assistant-turn-actions]'); + expect(actions?.classList.contains('opacity-100')).toBe(true); + expect(container.querySelector('[aria-label="Copy response"]')).not.toBeNull(); + expect(container.querySelector('[aria-label="Fork session"] .animate-spin')).not.toBeNull(); + + await act(async () => root.unmount()); + container.remove(); + }); +});