From d4fe3176ba0c28562d2997cf90d1eb7b89d7588f Mon Sep 17 00:00:00 2001
From: Jan Burzinski <156842394+janburzinski@users.noreply.github.com>
Date: Mon, 8 Jun 2026 21:17:37 +0200
Subject: [PATCH 1/4] feat(tasks): add archive on merge
---
src/main/core/settings/schema.ts | 1 +
src/main/core/settings/settings-registry.ts | 1 +
.../settings/components/SettingsPage.tsx | 4 +++
.../settings/components/TaskSettingsRows.tsx | 26 +++++++++++++++++++
.../features/tasks/hooks/useTaskSettings.ts | 7 +++++
.../features/tasks/stores/task-manager.ts | 15 +++++++++++
6 files changed, 54 insertions(+)
diff --git a/src/main/core/settings/schema.ts b/src/main/core/settings/schema.ts
index eb3312845a..95b0c249c1 100644
--- a/src/main/core/settings/schema.ts
+++ b/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/src/main/core/settings/settings-registry.ts b/src/main/core/settings/settings-registry.ts
index 3da98d0f6f..d3dffef96c 100644
--- a/src/main/core/settings/settings-registry.ts
+++ b/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/src/renderer/features/settings/components/SettingsPage.tsx b/src/renderer/features/settings/components/SettingsPage.tsx
index fba00bede4..092d74b155 100644
--- a/src/renderer/features/settings/components/SettingsPage.tsx
+++ b/src/renderer/features/settings/components/SettingsPage.tsx
@@ -17,6 +17,7 @@ import ResourceMonitorSettingsCard from './ResourceMonitorSettingsCard';
import SidebarMetadataSettingsCard from './SidebarMetadataSettingsCard';
import { SshConnectionsSettingsCard } from './SshConnectionsSettingsCard';
import {
+ ArchiveOnMergeRow,
AutoGenerateTaskNamesRow,
AutoTrustWorktreesRow,
CreateBranchAndWorktreeRow,
@@ -97,6 +98,9 @@ export function SettingsPage({
{
component: ,
},
+ {
+ component: ,
+ },
{
component: ,
},
diff --git a/src/renderer/features/settings/components/TaskSettingsRows.tsx b/src/renderer/features/settings/components/TaskSettingsRows.tsx
index 0b13391ed7..224673011d 100644
--- a/src/renderer/features/settings/components/TaskSettingsRows.tsx
+++ b/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/src/renderer/features/tasks/hooks/useTaskSettings.ts b/src/renderer/features/tasks/hooks/useTaskSettings.ts
index b346f24739..9029164bae 100644
--- a/src/renderer/features/tasks/hooks/useTaskSettings.ts
+++ b/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/src/renderer/features/tasks/stores/task-manager.ts b/src/renderer/features/tasks/stores/task-manager.ts
index cc0f5512ca..5225bb0d1b 100644
--- a/src/renderer/features/tasks/stores/task-manager.ts
+++ b/src/renderer/features/tasks/stores/task-manager.ts
@@ -7,9 +7,11 @@ import { getTaskGitStore } from '@renderer/features/tasks/stores/task-selectors'
import { events, rpc } from '@renderer/lib/ipc';
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 { prSyncProgressChannel, prUpdatedChannel } from '@shared/core/pull-requests/prEvents';
+import type { PullRequest } from '@shared/core/pull-requests/pull-requests';
import {
lifecycleScriptStatusChannel,
taskCreatedChannel,
@@ -226,6 +228,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);
+ });
}
}
});
@@ -263,6 +268,16 @@ export class TaskManagerStore {
(store.data as Task).prs = prs;
}
});
+ if (isRegistered(store)) {
+ await this._archiveTaskIfMerged(store.data as Task, prs);
+ }
+ }
+
+ 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 (!settings.archiveOnMerge) return;
+ await this.archiveTask(task.id);
}
loadTasks(): Promise {
From 419ae394eb5035dd3a3d45c668660d539a8da4fb Mon Sep 17 00:00:00 2001
From: Jan Burzinski <156842394+janburzinski@users.noreply.github.com>
Date: Tue, 9 Jun 2026 15:37:25 +0200
Subject: [PATCH 2/4] fix(tasks): leave archived task view
---
.../renderer/features/tasks/stores/task-manager.ts | 14 ++++++++++++++
.../src/renderer/features/tasks/view.tsx | 6 +++++-
2 files changed, 19 insertions(+), 1 deletion(-)
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 d8ddd63a9e..44f6fb80cc 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,6 +5,7 @@ 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';
@@ -288,9 +289,22 @@ export class TaskManagerStore {
if (task.archivedAt || !prs.some((pr) => pr.status === 'merged')) return;
const settings = (await rpc.appSettings.get('tasks')) as AppSettings['tasks'];
if (!settings.archiveOnMerge) return;
+ this._leaveTaskViewBeforeArchive(task.id);
await this.archiveTask(task.id);
}
+ private _leaveTaskViewBeforeArchive(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 {
if (!this._loadPromise) {
this._loadPromise = Promise.all([
diff --git a/apps/emdash-desktop/src/renderer/features/tasks/view.tsx b/apps/emdash-desktop/src/renderer/features/tasks/view.tsx
index e51307a5b4..75f220e1c6 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 };
From 38737593d2710b86e114c29e8c048ca3bb604b07 Mon Sep 17 00:00:00 2001
From: Jan Burzinski <156842394+janburzinski@users.noreply.github.com>
Date: Tue, 9 Jun 2026 15:59:32 +0200
Subject: [PATCH 3/4] fix(tasks): tighten archive-on-merge
---
.../features/tasks/stores/task-manager.ts | 21 ++++++++++++++-----
1 file changed, 16 insertions(+), 5 deletions(-)
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 44f6fb80cc..c25bcdc8bd 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
@@ -275,27 +275,38 @@ 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, prs);
+ await this._archiveTaskIfMerged(store.data as Task, newlyMergedPrs);
}
}
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 (!settings.archiveOnMerge) return;
- this._leaveTaskViewBeforeArchive(task.id);
+ if (task.archivedAt || !settings.archiveOnMerge) return;
await this.archiveTask(task.id);
+ this._leaveTaskViewAfterArchive(task.id);
}
- private _leaveTaskViewBeforeArchive(taskId: string): void {
+ private _leaveTaskViewAfterArchive(taskId: string): void {
const navigation = appState.navigation;
- const params = navigation.viewParamsStore.task as { projectId?: string; taskId?: string } | undefined;
+ const params = navigation.viewParamsStore.task as
+ | { projectId?: string; taskId?: string }
+ | undefined;
if (
navigation.currentViewId === 'task' &&
params?.projectId === this.projectId &&
From 0a2d5cbd39237595a1a13a6ae3870ee5dc607489 Mon Sep 17 00:00:00 2001
From: Jan Burzinski <156842394+janburzinski@users.noreply.github.com>
Date: Tue, 9 Jun 2026 17:25:31 +0200
Subject: [PATCH 4/4] fix(tasks): handle PR sync archive errors
---
.../src/renderer/features/tasks/stores/task-manager.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
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 c25bcdc8bd..41569bc4d1 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
@@ -290,7 +290,11 @@ export class TaskManagerStore {
previousPrStatuses.get(pr.url) !== 'merged'
);
if (isRegistered(store)) {
- await this._archiveTaskIfMerged(store.data as Task, newlyMergedPrs);
+ await this._archiveTaskIfMerged(store.data as Task, newlyMergedPrs).catch(
+ (error: unknown) => {
+ console.error('Failed to auto-archive merged task', error);
+ }
+ );
}
}