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); + } + ); } }