Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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: 'delete-worktree-and-branch' as const,
Comment thread
janburzinski marked this conversation as resolved.
Outdated
},
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="delete both"
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 @@ -91,6 +94,13 @@ 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>

{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 +144,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 ?? 'delete-worktree-and-branch',
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