Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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,45 @@ 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);
}
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