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
3 changes: 3 additions & 0 deletions apps/emdash-desktop/src/main/core/settings/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ export const notificationSettingsSchema = z.object({
soundFocusMode: z.enum(['always', 'unfocused']),
});

export const taskDeleteBehaviorSchema = z.enum(['delete-worktree-and-branch', 'ask']);

export const taskSettingsSchema = z.object({
autoGenerateName: z.boolean(),
autoTrustWorktrees: z.boolean(),
createBranchAndWorktree: z.boolean(),
preserveNameCapitalization: z.boolean(),
includeIssueContextByDefault: z.boolean(),
deleteBehavior: taskDeleteBehaviorSchema,
});

export const agentAutoApproveDefaultsSchema = z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const SETTINGS_DEFAULTS = {
createBranchAndWorktree: true,
preserveNameCapitalization: false,
includeIssueContextByDefault: true,
deleteBehavior: 'ask' as const,
},
agentAutoApproveDefaults: {},
notifications: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function deleteTask(
taskId: string,
options: DeleteTaskOptions = {}
): Promise<void> {
const { deleteWorktree = true, deleteBranch = false } = options;
const { deleteWorktree = true, deleteBranch = true } = options;

const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId)).limit(1);
if (!task) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
EnableTmuxRow,
IncludeIssueContextByDefaultRow,
PreserveTaskNameCapitalizationRow,
TaskDeleteBehaviorRow,
} from './TaskSettingsRows';
import TelemetryCard from './TelemetryCard';
import TerminalSettingsCard from './TerminalSettingsCard';
Expand Down Expand Up @@ -57,6 +58,7 @@ function GeneralSettingsPage() {
<CreateBranchAndWorktreeRow />
<PreserveTaskNameCapitalizationRow />
<IncludeIssueContextByDefaultRow />
<TaskDeleteBehaviorRow />
<EnableTmuxRow />
<NotificationSettingsCard />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@ import { Info } from 'lucide-react';
import React from 'react';
import { useAppSettingsKey } from '@renderer/features/settings/use-app-settings-key';
import { useTaskSettings } from '@renderer/features/tasks/hooks/useTaskSettings';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@renderer/lib/ui/select';
import { Switch } from '@renderer/lib/ui/switch';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@renderer/lib/ui/tooltip';
import type { TaskDeleteBehavior } from '@shared/core/app-settings';
import { ResetToDefaultButton } from './ResetToDefaultButton';
import { SettingRow } from './SettingRow';

Expand Down Expand Up @@ -166,6 +174,43 @@ export const IncludeIssueContextByDefaultRow: React.FC = () => {
);
};

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

return (
<SettingRow
title="Task delete behavior"
description="Choose whether deleting a task also deletes its worktree and branch automatically."
control={
<>
<ResetToDefaultButton
visible={taskSettings.isFieldOverridden('deleteBehavior')}
defaultLabel="ask"
onReset={taskSettings.resetDeleteBehavior}
disabled={taskSettings.loading || taskSettings.saving}
/>
<Select
value={taskSettings.deleteBehavior}
onValueChange={(next) => taskSettings.updateDeleteBehavior(next as TaskDeleteBehavior)}
>
<SelectTrigger className="w-auto shrink-0 gap-2 [&>span]:line-clamp-none">
<SelectValue>
{taskSettings.deleteBehavior === 'ask'
? 'Ask every time'
: 'Delete worktree and branch'}
</SelectValue>
</SelectTrigger>
<SelectContent align="end" className="min-w-max">
<SelectItem value="delete-worktree-and-branch">Delete worktree and branch</SelectItem>
<SelectItem value="ask">Ask every time</SelectItem>
</SelectContent>
</Select>
</>
}
/>
);
};

export const EnableTmuxRow: React.FC = () => {
const {
value: projects,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TriangleAlert } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useTaskSettings } from '@renderer/features/tasks/hooks/useTaskSettings';
import { rpc } from '@renderer/lib/ipc';
import type { BaseModalProps } from '@renderer/lib/modal/modal-provider';
import { Button } from '@renderer/lib/ui/button';
Expand Down Expand Up @@ -27,8 +28,9 @@ type Props = BaseModalProps<DeleteTaskModalResult> & DeleteTaskModalArgs;

export function DeleteTaskModal({ projectId, tasks, onSuccess, onClose }: Props) {
const [preflight, setPreflight] = useState<TaskDeletePreflightItem[] | null>(null);
const taskSettings = useTaskSettings();
const [deleteWorktree, setDeleteWorktree] = useState(true);
const [deleteBranch, setDeleteBranch] = useState(false);
const [deleteBranch, setDeleteBranch] = useState(true);

const count = tasks.length;
const isBulk = count > 1;
Expand All @@ -44,14 +46,15 @@ export function DeleteTaskModal({ projectId, tasks, onSuccess, onClose }: Props)
);
}, [projectId, taskIds]);

const isLoading = preflight === null;
const askWhatToDelete = taskSettings.deleteBehavior === 'ask';
const isLoading = preflight === null || taskSettings.loading;

const worktreeTasks = preflight?.filter((t) => t.hasWorktree) ?? [];
const dirtyTasks = preflight?.filter((t) => t.hasUncommittedChanges) ?? [];
const branchTasks = preflight?.filter((t) => t.hasDeletableBranch) ?? [];

const showWorktreeCheckbox = !isLoading && worktreeTasks.length > 0;
const showBranchCheckbox = !isLoading && branchTasks.length > 0;
const showWorktreeCheckbox = askWhatToDelete && !isLoading && worktreeTasks.length > 0;
const showBranchCheckbox = askWhatToDelete && !isLoading && branchTasks.length > 0;

const handleWorktreeChange = (checked: boolean) => {
setDeleteWorktree(checked);
Expand Down Expand Up @@ -83,6 +86,10 @@ export function DeleteTaskModal({ projectId, tasks, onSuccess, onClose }: Props)
return `${dirtyTasks.length} ${dirtyTasks.length === 1 ? 'task has' : 'tasks have'} uncommitted changes that will be lost: ${names}`;
})();

const deleteScopeNotice = isBulk
? 'Worktrees and branches for these tasks will also be deleted.'
: 'The worktree and branch for this task will also be deleted.';

return (
<>
<DialogHeader showCloseButton={false}>
Expand All @@ -91,6 +98,15 @@ export function DeleteTaskModal({ projectId, tasks, onSuccess, onClose }: Props)
<DialogContentArea className="flex flex-col gap-4 pt-0">
<p className="text-sm text-foreground-muted">{description}</p>

{!askWhatToDelete && <p className="text-xs text-foreground-muted">{deleteScopeNotice}</p>}

{dirtyWarning && !askWhatToDelete && (
<div className="flex items-start gap-1.5 rounded-md bg-background-warning px-3 py-2 text-xs text-foreground-warning">
<TriangleAlert className="mt-px size-3.5 shrink-0" />
<span>{dirtyWarning}</span>
</div>
)}
Comment thread
janburzinski marked this conversation as resolved.

{(showWorktreeCheckbox || showBranchCheckbox) && (
<div className="flex flex-col gap-3">
{showWorktreeCheckbox && (
Expand Down Expand Up @@ -134,7 +150,13 @@ export function DeleteTaskModal({ projectId, tasks, onSuccess, onClose }: Props)
<ConfirmButton
variant="destructive"
disabled={isLoading}
onClick={() => onSuccess({ deleteWorktree, deleteBranch })}
onClick={() =>
onSuccess(
askWhatToDelete
? { deleteWorktree, deleteBranch }
: { deleteWorktree: true, deleteBranch: true }
)
}
>
{isLoading ? 'Loading...' : isBulk ? `Delete ${count} tasks` : 'Delete'}
</ConfirmButton>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useAppSettingsKey } from '@renderer/features/settings/use-app-settings-key';
import type { TaskDeleteBehavior } from '@shared/core/app-settings';

export interface TaskSettingsModel {
autoGenerateName: boolean;
autoTrustWorktrees: boolean;
createBranchAndWorktree: boolean;
preserveNameCapitalization: boolean;
includeIssueContextByDefault: boolean;
deleteBehavior: TaskDeleteBehavior;
loading: boolean;
saving: boolean;
isFieldOverridden: (
Expand All @@ -15,17 +17,20 @@ export interface TaskSettingsModel {
| 'createBranchAndWorktree'
| 'preserveNameCapitalization'
| 'includeIssueContextByDefault'
| 'deleteBehavior'
) => boolean;
updateAutoGenerateName: (next: boolean) => void;
updateAutoTrustWorktrees: (next: boolean) => void;
updateCreateBranchAndWorktree: (next: boolean) => void;
updatePreserveNameCapitalization: (next: boolean) => void;
updateIncludeIssueContextByDefault: (next: boolean) => void;
updateDeleteBehavior: (next: TaskDeleteBehavior) => void;
resetAutoGenerateName: () => void;
resetAutoTrustWorktrees: () => void;
resetCreateBranchAndWorktree: () => void;
resetPreserveNameCapitalization: () => void;
resetIncludeIssueContextByDefault: () => void;
resetDeleteBehavior: () => void;
}

export function useTaskSettings(): TaskSettingsModel {
Expand All @@ -44,6 +49,7 @@ export function useTaskSettings(): TaskSettingsModel {
createBranchAndWorktree: tasks?.createBranchAndWorktree ?? true,
preserveNameCapitalization: tasks?.preserveNameCapitalization ?? false,
includeIssueContextByDefault: tasks?.includeIssueContextByDefault ?? true,
deleteBehavior: tasks?.deleteBehavior ?? 'ask',
loading,
saving,
isFieldOverridden,
Expand All @@ -52,10 +58,12 @@ export function useTaskSettings(): TaskSettingsModel {
updateCreateBranchAndWorktree: (next) => update({ createBranchAndWorktree: next }),
updatePreserveNameCapitalization: (next) => update({ preserveNameCapitalization: next }),
updateIncludeIssueContextByDefault: (next) => update({ includeIssueContextByDefault: next }),
updateDeleteBehavior: (next) => update({ deleteBehavior: next }),
resetAutoGenerateName: () => resetField('autoGenerateName'),
resetAutoTrustWorktrees: () => resetField('autoTrustWorktrees'),
resetCreateBranchAndWorktree: () => resetField('createBranchAndWorktree'),
resetPreserveNameCapitalization: () => resetField('preserveNameCapitalization'),
resetIncludeIssueContextByDefault: () => resetField('includeIssueContextByDefault'),
resetDeleteBehavior: () => resetField('deleteBehavior'),
};
}
2 changes: 2 additions & 0 deletions apps/emdash-desktop/src/shared/core/app-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type notificationSettingsSchema,
type projectSettingsSchema,
type providerCustomConfigEntrySchema,
type taskDeleteBehaviorSchema,
type taskSettingsSchema,
type terminalSettingsSchema,
type themeSchema,
Expand All @@ -18,6 +19,7 @@ export type LocalProjectSettings = z.infer<typeof localProjectSettingsSchema>;
export type ProjectSettings = z.infer<typeof projectSettingsSchema>;
export type NotificationSettings = z.infer<typeof notificationSettingsSchema>;
export type TaskSettings = z.infer<typeof taskSettingsSchema>;
export type TaskDeleteBehavior = z.infer<typeof taskDeleteBehaviorSchema>;
export type AgentAutoApproveDefaults = z.infer<typeof agentAutoApproveDefaultsSchema>;
export type TerminalSettings = z.infer<typeof terminalSettingsSchema>;
export type Theme = z.infer<typeof themeSchema>;
Expand Down
Loading