diff --git a/client/src/components/cos/tabs/RelaunchAgentModal.jsx b/client/src/components/cos/tabs/RelaunchAgentModal.jsx index 842834b40b..44820877bd 100644 --- a/client/src/components/cos/tabs/RelaunchAgentModal.jsx +++ b/client/src/components/cos/tabs/RelaunchAgentModal.jsx @@ -6,6 +6,7 @@ import Modal from '../../ui/Modal'; import AppContextPicker from '../../AppContextPicker'; import ProviderModelSelector from '../../ProviderModelSelector'; import { FormField } from '../../ui/FormField'; +import CollapsibleText from '../../ui/CollapsibleText'; import { useAsyncAction } from '../../../hooks/useAsyncAction'; import { effortAwareModelOptions, seedModelEffort } from '../../../utils/providers'; @@ -104,7 +105,20 @@ export default function RelaunchAgentModal({ agent, providers, apps, onDone, onC
Current task
-
{taskDescription}
+ {/* A task description is the agent's whole prompt — routinely hundreds of + lines. Rendered in full it pushes the provider/model selects and the + Relaunch button off a phone screen, so it opens clamped. Expanding + swaps in a height-capped scroll box rather than unclamping in place: + the point of the dialog is the controls below it, and an expanded + prompt must not bury them again. */} +
This stops the running agent and requeues the same task on the worktree it leaves behind — no second agent, and nothing to clean up afterward. diff --git a/client/src/components/cos/tabs/RelaunchAgentModal.test.jsx b/client/src/components/cos/tabs/RelaunchAgentModal.test.jsx index 67c6466100..1c3c314acf 100644 --- a/client/src/components/cos/tabs/RelaunchAgentModal.test.jsx +++ b/client/src/components/cos/tabs/RelaunchAgentModal.test.jsx @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -43,6 +43,12 @@ beforeEach(() => { api.relaunchCosAgent.mockResolvedValue({ success: true, taskId: 'task-abc', mode: 'requeued' }); }); +// A prototype getter spy would otherwise survive a failing assertion and leak +// into every test that runs after it. +afterEach(() => { + vi.restoreAllMocks(); +}); + describe('RelaunchAgentModal', () => { it('submits the stalled run\'s own settings when the user changes nothing', async () => { const user = userEvent.setup(); @@ -92,6 +98,31 @@ describe('RelaunchAgentModal', () => { expect(toast.success).not.toHaveBeenCalledWith(expect.stringMatching(/queued again/i)); }); + // A task description is the agent's whole prompt. Rendered in full it pushes + // the provider/model selects and the Relaunch button off a phone screen, so + // the dialog must open on a clamped preview and cap the expanded body's height. + it('opens the task prompt collapsed and caps it in a scroll box when expanded', async () => { + // jsdom reports 0 for scrollHeight and clientHeight alike, so nothing ever + // measures as overflowing without this. + vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(500); + const user = userEvent.setup(); + const prompt = Array.from({ length: 200 }, (_, i) => `step ${i}`).join('\n'); + renderModal({ agent: { ...STALLED_AGENT, metadata: { ...STALLED_AGENT.metadata, taskDescription: prompt } } }); + + const preview = document.getElementById('relaunch-task-agent-live'); + expect(preview.className).toContain('line-clamp-3'); + + await user.click(screen.getByRole('button', { name: /show more/i })); + + const expanded = document.getElementById('relaunch-task-agent-live'); + expect(expanded.className).not.toContain('line-clamp-3'); + expect(expanded.className).toContain('max-h-48'); + expect(expanded.className).toContain('overflow-y-auto'); + // The way back matters most on a phone — expanding must not strand the user + // in the prompt with the form below it out of reach. + expect(screen.getByRole('button', { name: /show less/i })).toBeInTheDocument(); + }); + it('keeps the dialog open and surfaces the error when the relaunch fails', async () => { const user = userEvent.setup(); const onClose = vi.fn();