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
16 changes: 15 additions & 1 deletion client/src/components/cos/tabs/RelaunchAgentModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -104,7 +105,20 @@ export default function RelaunchAgentModal({ agent, providers, apps, onDone, onC

<div className="mb-4 p-3 bg-port-bg border border-port-border rounded-lg">
<div className="text-sm text-gray-400 mb-1">Current task</div>
<div className="text-white">{taskDescription}</div>
{/* 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. */}
<CollapsibleText
id={`relaunch-task-${agent?.id || 'current'}`}
text={taskDescription}
lines={3}
className="text-white"
expandedContent={taskDescription}
expandedClassName="max-h-48 overflow-y-auto whitespace-pre-wrap"
/>
<div className="text-sm text-gray-400 mt-2">
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.
Expand Down
33 changes: 32 additions & 1 deletion client/src/components/cos/tabs/RelaunchAgentModal.test.jsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down