Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/emdash-desktop/src/main/core/settings/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const SETTINGS_DEFAULTS = {
createBranchAndWorktree: true,
preserveNameCapitalization: false,
includeIssueContextByDefault: true,
archiveOnMerge: false,
},
agentAutoApproveDefaults: {},
notifications: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ResourceMonitorSettingsCard from './ResourceMonitorSettingsCard';
import SidebarMetadataSettingsCard from './SidebarMetadataSettingsCard';
import { SshConnectionsSettingsCard } from './SshConnectionsSettingsCard';
import {
ArchiveOnMergeRow,
AutoGenerateTaskNamesRow,
AutoTrustWorktreesRow,
CreateBranchAndWorktreeRow,
Expand Down Expand Up @@ -96,6 +97,9 @@ export function SettingsPage({
{
component: <IncludeIssueContextByDefaultRow />,
},
{
component: <ArchiveOnMergeRow />,
},
{
component: <EnableTmuxRow />,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,32 @@ export const IncludeIssueContextByDefaultRow: React.FC = () => {
);
};

export const ArchiveOnMergeRow: React.FC = () => {
const taskSettings = useTaskSettings();

return (
<SettingRow
title="Archive on merge"
description="Automatically archive a task when Emdash detects its branch's pull request was merged."
control={
<>
<ResetToDefaultButton
visible={taskSettings.isFieldOverridden('archiveOnMerge')}
defaultLabel="off"
onReset={taskSettings.resetArchiveOnMerge}
disabled={taskSettings.loading || taskSettings.saving}
/>
<Switch
checked={taskSettings.archiveOnMerge}
disabled={taskSettings.loading || taskSettings.saving}
onCheckedChange={taskSettings.updateArchiveOnMerge}
/>
</>
}
/>
);
};

export const EnableTmuxRow: React.FC = () => {
const {
value: projects,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface TaskSettingsModel {
createBranchAndWorktree: boolean;
preserveNameCapitalization: boolean;
includeIssueContextByDefault: boolean;
archiveOnMerge: boolean;
loading: boolean;
saving: boolean;
isFieldOverridden: (
Expand All @@ -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 {
Expand All @@ -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,
Expand All @@ -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'),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
});
}
}
});
Expand Down Expand Up @@ -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);
}
);
}
Comment thread
janburzinski marked this conversation as resolved.
Comment thread
janburzinski marked this conversation as resolved.
}

private async _archiveTaskIfMerged(task: Task, prs: PullRequest[]): Promise<void> {
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);
}
Comment thread
janburzinski marked this conversation as resolved.

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<void> {
Expand Down
6 changes: 5 additions & 1 deletion apps/emdash-desktop/src/renderer/features/tasks/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
Loading