fix: keep remote recovery actions inside the project menu
Remove the duplicate recovery button beside New project and its extra layout column. Keep the collapsed failure status and the retry action on the affected project inside the dropdown. Stage complete files as requested, including existing App snapshot regression coverage and remote runtime design updates in those files. Validation: 29 ProjectSwitcher tests and the targeted App recovery test passed; typecheck and lint passed. Full test run timed out after 60 seconds. Release note: Remote recovery retries now stay in the project dropdown, keeping the project header stable when a connection fails.
This commit is contained in:
@@ -7124,6 +7124,35 @@ describe("App", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("restores live question controls and Stop from a Main snapshot without a local run", async () => {
|
||||
const requestId = "00000000-0000-4000-8000-000000000891";
|
||||
const messageId = "00000000-0000-4000-8000-000000000892";
|
||||
const conversationId = "00000000-0000-4000-8000-000000000893";
|
||||
vi.mocked(api.conversations.list).mockResolvedValue([{
|
||||
id: conversationId, projectId: project.id, title: "Recovered question", updatedAt: Date.now(),
|
||||
activeRequest: {
|
||||
requestId, messageId, questions: [{
|
||||
requestId, type: "question", questionId: "recovered-question",
|
||||
questions: [{ header: "Recovered", question: "Answer after restart?",
|
||||
options: [{ label: "Continue", description: "Resume only this Session" }], multiple: false, custom: true }],
|
||||
}],
|
||||
},
|
||||
messages: [{ id: messageId, role: "assistant", content: "Retained output", state: "streaming", createdAt: 1 }],
|
||||
}]);
|
||||
render(<App />);
|
||||
await screen.findByText("Answer after restart?");
|
||||
fireEvent.click(screen.getByRole("button", { name: "停止生成" }));
|
||||
await waitFor(() => expect(api.agent.cancel).toHaveBeenCalledWith(requestId));
|
||||
fireEvent.click(screen.getByRole("radio", { name: /Continue/u }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "提交回答" }));
|
||||
await waitFor(() => expect(api.agent.respondQuestion).toHaveBeenCalledWith(
|
||||
"recovered-question", [["Continue"]],
|
||||
));
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
expect(JSON.stringify(vi.mocked(api.conversations.saveLocal).mock.calls))
|
||||
.not.toContain('"activeRequest"');
|
||||
});
|
||||
|
||||
describe("persisted terminal state convergence", () => {
|
||||
let conversationsChangedListener: (() => void) | undefined;
|
||||
|
||||
@@ -7445,8 +7474,9 @@ describe("App", () => {
|
||||
"恢复失败:无法连接 Host",
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "当前项目" }));
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", {
|
||||
screen.getByRole("menuitem", {
|
||||
name: `重试恢复项目 ${remoteProject.name}`,
|
||||
}),
|
||||
);
|
||||
@@ -7455,6 +7485,7 @@ describe("App", () => {
|
||||
remoteProject.id,
|
||||
),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "当前项目" }));
|
||||
expect(await screen.findByText("正在恢复网络连接…")).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
|
||||
@@ -779,7 +779,7 @@ describe('ProjectSwitcher managed SSH projects', () => {
|
||||
expect(screen.queryByText('Recovery completed')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders an accessible failure and retries only the affected project', async () => {
|
||||
it('keeps recovery retry inside the project menu while showing the collapsed failure', async () => {
|
||||
await i18n.changeLanguage('en-US')
|
||||
const remoteProject: AssistantProject = {
|
||||
...project,
|
||||
@@ -822,14 +822,22 @@ describe('ProjectSwitcher managed SSH projects', () => {
|
||||
expect(screen.getAllByRole('alert')[0]).toHaveTextContent(
|
||||
'Recovery failed: Host unreachable'
|
||||
)
|
||||
expect(screen.queryByRole('button', {
|
||||
name: 'Retry recovery for project Remote project'
|
||||
})).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Current project' }))
|
||||
const menu = screen.getByRole('menu', { name: 'Current project' })
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
within(menu).getByRole('menuitem', {
|
||||
name: 'Retry recovery for project Remote project'
|
||||
})
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(onRetryRecovery).toHaveBeenCalledWith(remoteProject.id)
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Current project' }))
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
|
||||
expect(screen.queryByLabelText('Retry recovery for project Remote project')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides remote projects and APIs when the feature is disabled', async () => {
|
||||
|
||||
@@ -1164,14 +1164,7 @@ export function ProjectSwitcher({
|
||||
|
||||
return (
|
||||
<div className="project-switcher">
|
||||
<div
|
||||
className={`project-switcher__row${
|
||||
activeProjectRecovery?.stage === 'failed' &&
|
||||
activeProjectRecovery.retryable
|
||||
? ' project-switcher__row--with-retry'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<div className="project-switcher__row">
|
||||
<div
|
||||
className="project-switcher__picker"
|
||||
ref={projectPickerRef}
|
||||
@@ -1346,26 +1339,6 @@ export function ProjectSwitcher({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{activeProject &&
|
||||
activeProjectRecovery?.stage === 'failed' &&
|
||||
activeProjectRecovery.retryable &&
|
||||
onRetryRecovery && (
|
||||
<button
|
||||
aria-label={t(
|
||||
'projectSwitcher.recovery.retryNamed',
|
||||
{ name: activeProjectDisplay?.name }
|
||||
)}
|
||||
className="icon-button project-switcher__active-retry"
|
||||
disabled={
|
||||
retryingRecoveryProjectId === activeProject.id
|
||||
}
|
||||
onClick={() => retryRecovery(activeProject.id)}
|
||||
title={t('projectSwitcher.recovery.retry')}
|
||||
type="button"
|
||||
>
|
||||
<RefreshCw aria-hidden="true" size={15} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
aria-label={t('projectSwitcher.selector.create')}
|
||||
className="icon-button"
|
||||
|
||||
@@ -2560,10 +2560,6 @@ textarea:focus-visible {
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
.project-switcher__row--with-retry {
|
||||
grid-template-columns: minmax(0, 1fr) auto auto auto;
|
||||
}
|
||||
|
||||
.project-switcher__picker {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -2812,10 +2808,6 @@ textarea:focus-visible {
|
||||
background: var(--danger-subtle);
|
||||
}
|
||||
|
||||
.project-switcher__active-retry {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.project-recovery-status {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
|
||||
Reference in New Issue
Block a user