Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/components/src/components/ai-gui/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3629,7 +3629,7 @@ const AssistantForkButton = ({
);
};

const AssistantTurnFooter = ({
export const AssistantTurnFooter = ({
message,
sessionId,
fileDiffOverride,
Expand Down Expand Up @@ -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
>
Expand Down
18 changes: 18 additions & 0 deletions packages/components/src/stories/AssistantTurnAlignment.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,24 @@ export const DesktopFinishedTurn: Story = {
),
};

export const DesktopForkingTurn: Story = {
args: { sessionId, items: finishedItems, renderMessageRow },
globals: { theme: 'dark' },
render: () => (
<div className="h-[520px] w-full bg-background">
<SessionChatStreamView
items={finishedItems}
sessionId={sessionId}
renderMessageRow={renderMessageRow}
lastAssistantMessageId={finishedTurn.id}
lastCompletedAssistantMessageId={finishedTurn.id}
onForkLastAssistant={() => undefined}
forkingAssistantMessageId={finishedTurn.id}
/>
</div>
),
};

/**
* PLAN-MODE TURN — the widest set of top-level row shells in one column.
*
Expand Down
58 changes: 57 additions & 1 deletion packages/components/tests/assistant-turn-action-inset.test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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();
});
});
Loading