diff --git a/frontend/e2e/flows.spec.ts b/frontend/e2e/flows.spec.ts index 2285d6da4..b5c9bcb82 100644 --- a/frontend/e2e/flows.spec.ts +++ b/frontend/e2e/flows.spec.ts @@ -712,12 +712,8 @@ for (const variant of TARGET_VARIANTS) { const starBtn = page.getByTestId(`star-btn-${newConversationId}`); await expect(starBtn).toBeVisible({ timeout: 5_000 }); - // Promote via backend API directly (UI handler targets a mismatched path) - const promoteResp = await request.post( - `/api/attacks/${encodeURIComponent(attackResultId)}/update-main-conversation`, - { data: { conversation_id: newConversationId } }, - ); - expect(promoteResp.ok()).toBeTruthy(); + // Promote via the UI star button (tests the full click → API → refresh flow) + await starBtn.click(); await expect .poll( @@ -889,17 +885,10 @@ for (const variant of TARGET_VARIANTS) { timeout: 5_000, }); - // Verify star button is visible but promote via API directly - // (UI handler targets a mismatched endpoint path) - await expect( - page.getByTestId(`star-btn-${branchConversationId}`), - ).toBeVisible({ timeout: 5_000 }); - - const promoteResp = await request.post( - `/api/attacks/${encodeURIComponent(attackResultId)}/update-main-conversation`, - { data: { conversation_id: branchConversationId } }, - ); - expect(promoteResp.ok()).toBeTruthy(); + // Promote via the UI star button (tests the full click → API → refresh flow) + const starBtn = page.getByTestId(`star-btn-${branchConversationId}`); + await expect(starBtn).toBeVisible({ timeout: 5_000 }); + await starBtn.click(); await expect .poll( diff --git a/frontend/src/components/Chat/ChatWindow.tsx b/frontend/src/components/Chat/ChatWindow.tsx index e7e57cbd6..92249fa02 100644 --- a/frontend/src/components/Chat/ChatWindow.tsx +++ b/frontend/src/components/Chat/ChatWindow.tsx @@ -391,6 +391,7 @@ export default function ChatWindow({ try { await attacksApi.changeMainConversation(attackResultId, convId) + setPanelRefreshKey(k => k + 1) } catch (err) { console.error('Failed to change main conversation:', err) } @@ -528,7 +529,12 @@ export default function ChatWindow({ onNewConversation={handleNewConversation} onChangeMainConversation={handleChangeMainConversation} onClose={() => setIsPanelOpen(false)} - locked={!activeTarget || isOperatorLocked || isCrossTargetLocked} + lockedReason={ + !activeTarget ? 'Configure a target to enable this action.' + : isOperatorLocked ? 'Cannot modify — attack belongs to a different operator.' + : isCrossTargetLocked ? 'Cannot modify — attack was created with a different target.' + : undefined + } refreshKey={panelRefreshKey} /> )} diff --git a/frontend/src/components/Chat/ConversationPanel.tsx b/frontend/src/components/Chat/ConversationPanel.tsx index 23072c1de..267b0feaf 100644 --- a/frontend/src/components/Chat/ConversationPanel.tsx +++ b/frontend/src/components/Chat/ConversationPanel.tsx @@ -30,8 +30,8 @@ interface ConversationPanelProps { onNewConversation: () => void onChangeMainConversation: (conversationId: string) => void onClose: () => void - /** When true, disable mutating actions (new conversation, promote to main) */ - locked?: boolean + /** When set, disables mutating actions (new conversation, promote to main) and explains why. */ + lockedReason?: string /** Increment to trigger a conversation list refresh (e.g. after sending a message) */ refreshKey?: number } @@ -43,7 +43,7 @@ export default function ConversationPanel({ onNewConversation, onChangeMainConversation, onClose, - locked, + lockedReason, refreshKey, }: ConversationPanelProps) { const styles = useConversationPanelStyles() @@ -107,13 +107,13 @@ export default function ConversationPanel({ )}
- +