diff --git a/apps/emdash-desktop/src/main/core/settings/schema.ts b/apps/emdash-desktop/src/main/core/settings/schema.ts
index 248834fc6..40d1a6557 100644
--- a/apps/emdash-desktop/src/main/core/settings/schema.ts
+++ b/apps/emdash-desktop/src/main/core/settings/schema.ts
@@ -37,6 +37,7 @@ export const taskSettingsSchema = z.object({
createBranchAndWorktree: z.boolean(),
preserveNameCapitalization: z.boolean(),
includeIssueContextByDefault: z.boolean(),
+ archiveOnMerge: z.boolean(),
});
export const agentAutoApproveDefaultsSchema = z
diff --git a/apps/emdash-desktop/src/main/core/settings/settings-registry.ts b/apps/emdash-desktop/src/main/core/settings/settings-registry.ts
index 20464a378..afd1b6f82 100644
--- a/apps/emdash-desktop/src/main/core/settings/settings-registry.ts
+++ b/apps/emdash-desktop/src/main/core/settings/settings-registry.ts
@@ -29,6 +29,7 @@ export const SETTINGS_DEFAULTS = {
createBranchAndWorktree: true,
preserveNameCapitalization: false,
includeIssueContextByDefault: true,
+ archiveOnMerge: false,
},
agentAutoApproveDefaults: {},
notifications: {
diff --git a/apps/emdash-desktop/src/renderer/features/settings/components/SettingsPage.tsx b/apps/emdash-desktop/src/renderer/features/settings/components/SettingsPage.tsx
index 78d135307..40f504f8b 100644
--- a/apps/emdash-desktop/src/renderer/features/settings/components/SettingsPage.tsx
+++ b/apps/emdash-desktop/src/renderer/features/settings/components/SettingsPage.tsx
@@ -16,6 +16,7 @@ import ResourceMonitorSettingsCard from './ResourceMonitorSettingsCard';
import SidebarMetadataSettingsCard from './SidebarMetadataSettingsCard';
import { SshConnectionsSettingsCard } from './SshConnectionsSettingsCard';
import {
+ ArchiveOnMergeRow,
AutoGenerateTaskNamesRow,
AutoTrustWorktreesRow,
CreateBranchAndWorktreeRow,
@@ -96,6 +97,9 @@ export function SettingsPage({
{
component: ,
},
+ {
+ component: ,
+ },
{
component: ,
},
diff --git a/apps/emdash-desktop/src/renderer/features/settings/components/TaskSettingsRows.tsx b/apps/emdash-desktop/src/renderer/features/settings/components/TaskSettingsRows.tsx
index 0b13391ed..224673011 100644
--- a/apps/emdash-desktop/src/renderer/features/settings/components/TaskSettingsRows.tsx
+++ b/apps/emdash-desktop/src/renderer/features/settings/components/TaskSettingsRows.tsx
@@ -166,6 +166,32 @@ export const IncludeIssueContextByDefaultRow: React.FC = () => {
);
};
+export const ArchiveOnMergeRow: React.FC = () => {
+ const taskSettings = useTaskSettings();
+
+ return (
+
+
+
+ >
+ }
+ />
+ );
+};
+
export const EnableTmuxRow: React.FC = () => {
const {
value: projects,
diff --git a/apps/emdash-desktop/src/renderer/features/tasks/hooks/useTaskSettings.ts b/apps/emdash-desktop/src/renderer/features/tasks/hooks/useTaskSettings.ts
index b346f2473..9029164ba 100644
--- a/apps/emdash-desktop/src/renderer/features/tasks/hooks/useTaskSettings.ts
+++ b/apps/emdash-desktop/src/renderer/features/tasks/hooks/useTaskSettings.ts
@@ -6,6 +6,7 @@ export interface TaskSettingsModel {
createBranchAndWorktree: boolean;
preserveNameCapitalization: boolean;
includeIssueContextByDefault: boolean;
+ archiveOnMerge: boolean;
loading: boolean;
saving: boolean;
isFieldOverridden: (
@@ -15,17 +16,20 @@ export interface TaskSettingsModel {
| 'createBranchAndWorktree'
| 'preserveNameCapitalization'
| 'includeIssueContextByDefault'
+ | 'archiveOnMerge'
) => boolean;
updateAutoGenerateName: (next: boolean) => void;
updateAutoTrustWorktrees: (next: boolean) => void;
updateCreateBranchAndWorktree: (next: boolean) => void;
updatePreserveNameCapitalization: (next: boolean) => void;
updateIncludeIssueContextByDefault: (next: boolean) => void;
+ updateArchiveOnMerge: (next: boolean) => void;
resetAutoGenerateName: () => void;
resetAutoTrustWorktrees: () => void;
resetCreateBranchAndWorktree: () => void;
resetPreserveNameCapitalization: () => void;
resetIncludeIssueContextByDefault: () => void;
+ resetArchiveOnMerge: () => void;
}
export function useTaskSettings(): TaskSettingsModel {
@@ -44,6 +48,7 @@ export function useTaskSettings(): TaskSettingsModel {
createBranchAndWorktree: tasks?.createBranchAndWorktree ?? true,
preserveNameCapitalization: tasks?.preserveNameCapitalization ?? false,
includeIssueContextByDefault: tasks?.includeIssueContextByDefault ?? true,
+ archiveOnMerge: tasks?.archiveOnMerge ?? false,
loading,
saving,
isFieldOverridden,
@@ -52,10 +57,12 @@ export function useTaskSettings(): TaskSettingsModel {
updateCreateBranchAndWorktree: (next) => update({ createBranchAndWorktree: next }),
updatePreserveNameCapitalization: (next) => update({ preserveNameCapitalization: next }),
updateIncludeIssueContextByDefault: (next) => update({ includeIssueContextByDefault: next }),
+ updateArchiveOnMerge: (next) => update({ archiveOnMerge: next }),
resetAutoGenerateName: () => resetField('autoGenerateName'),
resetAutoTrustWorktrees: () => resetField('autoTrustWorktrees'),
resetCreateBranchAndWorktree: () => resetField('createBranchAndWorktree'),
resetPreserveNameCapitalization: () => resetField('preserveNameCapitalization'),
resetIncludeIssueContextByDefault: () => resetField('includeIssueContextByDefault'),
+ resetArchiveOnMerge: () => resetField('archiveOnMerge'),
};
}
diff --git a/apps/emdash-desktop/src/renderer/features/tasks/stores/task-manager.ts b/apps/emdash-desktop/src/renderer/features/tasks/stores/task-manager.ts
index 3ff144aab..41569bc4d 100644
--- a/apps/emdash-desktop/src/renderer/features/tasks/stores/task-manager.ts
+++ b/apps/emdash-desktop/src/renderer/features/tasks/stores/task-manager.ts
@@ -5,12 +5,15 @@ import type { ProjectSettingsStore } from '@renderer/features/projects/stores/pr
import type { RepositoryStore } from '@renderer/features/projects/stores/repository-store';
import { getTaskGitStore } from '@renderer/features/tasks/stores/task-selectors';
import { events, rpc } from '@renderer/lib/ipc';
+import { appState } from '@renderer/lib/stores/app-state';
import { viewStateCache } from '@renderer/lib/stores/view-state-cache';
import type { AgentProviderId } from '@shared/core/agents/agent-provider-registry';
+import type { AppSettings } from '@shared/core/app-settings';
import type { Conversation } from '@shared/core/conversations/conversations';
import type { FetchError } from '@shared/core/git/git';
import { gitWorkspaceChangedChannel } from '@shared/core/git/gitEvents';
import { prSyncProgressChannel, prUpdatedChannel } from '@shared/core/pull-requests/prEvents';
+import type { PullRequest } from '@shared/core/pull-requests/pull-requests';
import {
lifecycleScriptStatusChannel,
taskCreatedChannel,
@@ -228,6 +231,9 @@ export class TaskManagerStore {
task.prs.push(pr);
}
});
+ void this._archiveTaskIfMerged(task, [pr]).catch((error: unknown) => {
+ console.error('Failed to auto-archive merged task', error);
+ });
}
}
});
@@ -269,11 +275,49 @@ export class TaskManagerStore {
const result = await rpc.pullRequests.getPullRequestsForTask(this.projectId, store.data.id);
if (!result.success) return;
const prs = result.data.prs;
+ const previousPrStatuses = new Map(
+ isRegistered(store) ? (store.data as Task).prs.map((pr) => [pr.url, pr.status]) : []
+ );
runInAction(() => {
if (isRegistered(store)) {
(store.data as Task).prs = prs;
}
});
+ const newlyMergedPrs = prs.filter(
+ (pr) =>
+ pr.status === 'merged' &&
+ previousPrStatuses.get(pr.url) !== undefined &&
+ previousPrStatuses.get(pr.url) !== 'merged'
+ );
+ if (isRegistered(store)) {
+ await this._archiveTaskIfMerged(store.data as Task, newlyMergedPrs).catch(
+ (error: unknown) => {
+ console.error('Failed to auto-archive merged task', error);
+ }
+ );
+ }
+ }
+
+ private async _archiveTaskIfMerged(task: Task, prs: PullRequest[]): Promise {
+ if (task.archivedAt || !prs.some((pr) => pr.status === 'merged')) return;
+ const settings = (await rpc.appSettings.get('tasks')) as AppSettings['tasks'];
+ if (task.archivedAt || !settings.archiveOnMerge) return;
+ await this.archiveTask(task.id);
+ this._leaveTaskViewAfterArchive(task.id);
+ }
+
+ private _leaveTaskViewAfterArchive(taskId: string): void {
+ const navigation = appState.navigation;
+ const params = navigation.viewParamsStore.task as
+ | { projectId?: string; taskId?: string }
+ | undefined;
+ if (
+ navigation.currentViewId === 'task' &&
+ params?.projectId === this.projectId &&
+ params.taskId === taskId
+ ) {
+ navigation.navigate('project', { projectId: this.projectId });
+ }
}
loadTasks(): Promise {
diff --git a/apps/emdash-desktop/src/renderer/features/tasks/view.tsx b/apps/emdash-desktop/src/renderer/features/tasks/view.tsx
index e51307a5b..75f220e1c 100644
--- a/apps/emdash-desktop/src/renderer/features/tasks/view.tsx
+++ b/apps/emdash-desktop/src/renderer/features/tasks/view.tsx
@@ -76,7 +76,11 @@ export const taskView = {
return { ok: false, redirect: 'home' };
}
const taskManager = getTaskManagerStore(projectId);
- if (taskManager && !taskManager.tasks.has(taskId)) {
+ const taskStore = taskManager?.tasks.get(taskId);
+ if (taskManager && !taskStore) {
+ return { ok: false, redirect: 'project', params: { projectId } };
+ }
+ if (taskStore && 'archivedAt' in taskStore.data && taskStore.data.archivedAt) {
return { ok: false, redirect: 'project', params: { projectId } };
}
return { ok: true };