mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
fix(agents): restore archived tasks for continued conversation (#349)
Archived agent tasks could be opened but never returned to the active list, so their message composer stayed disabled. Unarchive is now available in the task header, in the archived-task notice above the composer, and in the task context menu (including mixed bulk selections). Restoring a task moves it back to the active list, keeps its conversation and worktree, and reconnects the existing agent session on the next message; the backend persists and broadcasts the restored status and refuses restoration while a stopped turn is still settling.
This commit is contained in:
@@ -112,6 +112,7 @@ export const commands = {
|
||||
/** Answers a permission request; `option: null` cancels it. */
|
||||
agentRespond: (task: string, permission: number, option: string | null) => typedError<null, IdeError>(__TAURI_INVOKE("agent_respond", { task, permission, option })),
|
||||
agentArchive: (task: string) => typedError<null, IdeError>(__TAURI_INVOKE("agent_archive", { task })),
|
||||
agentUnarchive: (task: string) => typedError<null, IdeError>(__TAURI_INVOKE("agent_unarchive", { task })),
|
||||
agentDelete: (task: string, removeWorktree: boolean) => typedError<null, IdeError>(__TAURI_INVOKE("agent_delete", { task, removeWorktree })),
|
||||
agentChanges: (task: string) => typedError<AgentChange[], IdeError>(__TAURI_INVOKE("agent_changes", { task })),
|
||||
/** Content of `path` when the task started (left side of its diff). */
|
||||
|
||||
@@ -7,7 +7,7 @@ import { confirmChecked, showError } from "@/workbench/messages";
|
||||
import { ContextMenuHost, hideContextMenu } from "@/ui";
|
||||
import { AgentsView } from "./AgentsView";
|
||||
import { SESSION_KIND } from "./model";
|
||||
import { archiveTask, deleteTask, openTask, useAgents } from "./store";
|
||||
import { archiveTask, unarchiveTask, deleteTask, openTask, useAgents } from "./store";
|
||||
import { taskActions } from "./taskActions";
|
||||
|
||||
const editor = vi.hoisted(() => ({
|
||||
@@ -32,6 +32,7 @@ vi.mock("./store", async (original) => ({
|
||||
...(await original<typeof import("./store")>()),
|
||||
openTask: vi.fn((id: string) => editor.listener?.({ kind: "agent-session", key: id, data: { task: id } })),
|
||||
archiveTask: vi.fn(async () => {}),
|
||||
unarchiveTask: vi.fn(async () => {}),
|
||||
deleteTask: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
@@ -183,6 +184,19 @@ describe("Agents bulk actions", () => {
|
||||
await waitFor(() => expect(archiveTask).toHaveBeenCalledExactlyOnceWith("two"));
|
||||
});
|
||||
|
||||
it("unarchives only archived tasks in a mixed selection", async () => {
|
||||
const tasks = [task("one"), task("two", "archived"), task("three", "archived")];
|
||||
useAgents.setState({ tasks: new Map(tasks.map((t) => [t.id, t])) });
|
||||
setup();
|
||||
fireEvent.doubleClick(screen.getByText("Archived"));
|
||||
select("one");
|
||||
select("three", { shiftKey: true });
|
||||
context("two");
|
||||
fireEvent.click(screen.getByRole("menuitem", { name: "Unarchive (2)" }));
|
||||
await waitFor(() => expect(unarchiveTask).toHaveBeenCalledTimes(2));
|
||||
expect(vi.mocked(unarchiveTask).mock.calls).toEqual([["two"], ["three"]]);
|
||||
});
|
||||
|
||||
it("cancels the whole batch", async () => {
|
||||
vi.mocked(confirmChecked).mockResolvedValue(null);
|
||||
await taskActions.find((action) => action.id === "agents.deleteTasks")!.run({ ctx: {}, arg: ["one", "two"] });
|
||||
|
||||
@@ -216,6 +216,15 @@ function Header({ task, entries, head }: { task: AgentTask; entries: AgentEntry[
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{task.status === "archived" && (
|
||||
<IconButton
|
||||
icon="undo"
|
||||
title="Unarchive Task"
|
||||
size="small"
|
||||
action="agents.unarchiveTasks"
|
||||
actionArg={[task.id]}
|
||||
/>
|
||||
)}
|
||||
{!busy && task.status !== "archived" && (
|
||||
<IconButton icon="close" title="Archive Task" size="small" onClick={() => void archiveTask(task.id)} />
|
||||
)}
|
||||
@@ -1064,6 +1073,16 @@ function SessionComposer({
|
||||
);
|
||||
return (
|
||||
<div className={styles.composerArea}>
|
||||
{task.status === "archived" && (
|
||||
<div className={styles.banner}>
|
||||
<span className={styles.bannerText}>
|
||||
<T>This task is archived. Unarchive it to continue the conversation.</T>
|
||||
</span>
|
||||
<Button size="small" onClick={() => void executeAction("agents.unarchiveTasks", [task.id])}>
|
||||
Unarchive
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<WorkingSubagents entries={entries} onOpen={onOpenSubagent} />
|
||||
{task.queue.length > 0 && <Queue task={task} />}
|
||||
<Composer
|
||||
|
||||
@@ -489,5 +489,6 @@ export function addReferences(refs: Array<{ path: string; rel: string; mention:
|
||||
export const cancelTask = (task: string, subagent: string | null = null) => call(commands.agentCancel(task, subagent));
|
||||
export const respond = (task: string, permission: number, option: string | null) =>
|
||||
call(commands.agentRespond(task, permission, option));
|
||||
export const unarchiveTask = (task: string) => call(commands.agentUnarchive(task));
|
||||
export const archiveTask = (task: string) => call(commands.agentArchive(task));
|
||||
export const deleteTask = (task: string, removeWorktree: boolean) => call(commands.agentDelete(task, removeWorktree));
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tx } from "@/platform/i18n";
|
||||
import type { MenuItem } from "@/ui";
|
||||
import { showError, confirmChecked } from "@/workbench/messages";
|
||||
import { isBusy } from "./model";
|
||||
import { archiveTask, deleteTask, openTask, useAgents } from "./store";
|
||||
import { archiveTask, unarchiveTask, deleteTask, openTask, useAgents } from "./store";
|
||||
|
||||
const canArchive = (task: AgentTask) => !isBusy(task.status) && task.status !== "archived";
|
||||
|
||||
@@ -75,6 +75,18 @@ export const taskActions: Action[] = [
|
||||
run: ({ arg }) =>
|
||||
runTasks(resolveTasks(arg).filter(canArchive), (task) => archiveTask(task.id), tx("Cannot archive agent tasks")),
|
||||
},
|
||||
{
|
||||
id: "agents.unarchiveTasks",
|
||||
title: "Unarchive Agent Tasks",
|
||||
category: "Agents",
|
||||
hidden: true,
|
||||
run: ({ arg }) =>
|
||||
runTasks(
|
||||
resolveTasks(arg).filter((task) => task.status === "archived"),
|
||||
(task) => unarchiveTask(task.id),
|
||||
tx("Cannot unarchive agent tasks"),
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "agents.deleteTasks",
|
||||
title: "Delete Agent Tasks",
|
||||
@@ -89,6 +101,7 @@ export function taskMenu(tasks: AgentTask[]): MenuItem[] {
|
||||
const ids = tasks.map((task) => task.id);
|
||||
const task = tasks[0]!;
|
||||
const multiple = tasks.length > 1;
|
||||
const archived = tasks.filter((task) => task.status === "archived").length;
|
||||
const archivable = tasks.filter(canArchive).length;
|
||||
return [
|
||||
...(!multiple
|
||||
@@ -98,6 +111,16 @@ export function taskMenu(tasks: AgentTask[]): MenuItem[] {
|
||||
{ kind: "separator" },
|
||||
] satisfies MenuItem[])
|
||||
: []),
|
||||
...(archived
|
||||
? ([
|
||||
{
|
||||
kind: "action",
|
||||
action: "agents.unarchiveTasks",
|
||||
label: multiple ? tx`Unarchive (${archived})` : "Unarchive",
|
||||
arg: ids,
|
||||
},
|
||||
] satisfies MenuItem[])
|
||||
: []),
|
||||
{
|
||||
kind: "action",
|
||||
action: "agents.archiveTasks",
|
||||
|
||||
@@ -4899,5 +4899,11 @@
|
||||
"Credentials the IDE answers git's HTTPS prompts with, stored in the app's own secret file (not git's credential.helper). A rejected credential is forgotten automatically.": "IDE 用于应答 git HTTPS 提示的凭据,存储在应用自己的密钥文件中(而非 git 的 credential.helper)。被拒绝的凭据会自动删除。",
|
||||
"Saved git credentials": "已保存的 git 凭据",
|
||||
"Forget": "忘记",
|
||||
"Remember for this host": "记住此主机"
|
||||
"Remember for this host": "记住此主机",
|
||||
"Unarchive Task": "取消归档任务",
|
||||
"This task is archived. Unarchive it to continue the conversation.": "此任务已归档。取消归档后即可继续对话。",
|
||||
"Unarchive": "取消归档",
|
||||
"Unarchive Agent Tasks": "取消归档 Agent 任务",
|
||||
"Cannot unarchive agent tasks": "无法取消归档 Agent 任务",
|
||||
"Unarchive ({archived})": "取消归档({archived})"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user