diff --git a/apps/staged/src/lib/features/projects/ProjectContextMenu.svelte b/apps/staged/src/lib/features/projects/ProjectContextMenu.svelte
new file mode 100644
index 00000000..796e1d8d
--- /dev/null
+++ b/apps/staged/src/lib/features/projects/ProjectContextMenu.svelte
@@ -0,0 +1,211 @@
+
+
+
+
+
diff --git a/apps/staged/src/lib/features/projects/ProjectHome.svelte b/apps/staged/src/lib/features/projects/ProjectHome.svelte
index 0a610e8d..d8a4dde5 100644
--- a/apps/staged/src/lib/features/projects/ProjectHome.svelte
+++ b/apps/staged/src/lib/features/projects/ProjectHome.svelte
@@ -24,6 +24,8 @@
import { workspaceLifecycle } from './workspaceLifecycle.svelte';
import { projectRunActionsStore } from '../../stores/projectRunActions.svelte';
import { repoBadgeStore } from '../../stores/repoBadges.svelte';
+ import { projectStateStore } from '../../stores/projectState.svelte';
+ import { canDeleteProjectWithoutConfirmation } from './projectDeleteSafety';
/**
* Merge incoming branches with existing ones, preserving worktreePath when
@@ -376,29 +378,14 @@
continue;
}
- // Check if all branches have merged PRs and no unpushed changes.
- // Skip the expensive hasUnpushedCommits check for remote branches —
- // their commits live on the workspace and the SSH round-trip (~5s)
- // blocks the UI. Treat merged remote branches as safe.
- if (branches.length > 0) {
- const allSafe = await Promise.all(
- branches.map(async (branch) => {
- const isMerged = branch.prState === 'MERGED';
- if (!isMerged) return false;
- if (branch.branchType === 'remote') return true;
-
- try {
- const hasUnpushed = await commands.hasUnpushedCommits(branch.id);
- return !hasUnpushed;
- } catch (e) {
- return false;
- }
- })
- );
+ const safe = await canDeleteProjectWithoutConfirmation({
+ branches,
+ repoCount,
+ hasUnpushedCommits: commands.hasUnpushedCommits,
+ });
- if (allSafe.every((safe) => safe)) {
- nextSafe.add(project.id);
- }
+ if (safe) {
+ nextSafe.add(project.id);
}
}
@@ -420,6 +407,11 @@
showNewProjectModal = true;
}
+ function handleMarkProjectUnread(project: Project) {
+ if (deletingProjectNames.has(project.id)) return;
+ projectStateStore.markAsUnread(project.id);
+ }
+
async function handleProjectCreated(project: Project) {
if (!projects.some((p) => p.id === project.id)) {
projects = [...projects, project];
@@ -444,35 +436,12 @@
const branches = branchesByProject.get(project.id) || [];
const repoCount = repoCountsByProject.get(project.id) || 0;
- // If no repos, safe to delete without confirmation
- if (repoCount === 0) {
- projectToDelete = project;
- // Immediately confirm since it's safe
- await confirmDeleteProject();
- return;
- }
-
- // Check if all branches have merged PRs and no unpushed changes.
- // Skip the expensive hasUnpushedCommits check for remote branches —
- // their commits live on the workspace and the SSH round-trip blocks the UI.
- const allSafe = await Promise.all(
- branches.map(async (branch) => {
- const isMerged = branch.prState === 'MERGED';
- if (!isMerged) return false;
- if (branch.branchType === 'remote') return true;
-
- // Check for unpushed commits
- try {
- const hasUnpushed = await commands.hasUnpushedCommits(branch.id);
- return !hasUnpushed;
- } catch (e) {
- console.error('Failed to check unpushed commits:', e);
- return false;
- }
- })
- );
-
- const isSafeToDelete = branches.length > 0 && allSafe.every((safe) => safe);
+ const isSafeToDelete = await canDeleteProjectWithoutConfirmation({
+ branches,
+ repoCount,
+ hasUnpushedCommits: commands.hasUnpushedCommits,
+ onCheckError: (e) => console.error('Failed to check unpushed commits:', e),
+ });
if (isSafeToDelete) {
// Safe to delete without confirmation
@@ -511,6 +480,7 @@
try {
await commands.deleteProject(id);
+ projectStateStore.markAsRead(id);
projects = projects.filter((p) => p.id !== id);
setProjects(projects);
const nextBranches = new Map(branchesByProject);
@@ -690,6 +660,8 @@
{reposByProject}
projectBranches={branchesByProject}
showAllProjectsRow={true}
+ onMarkProjectUnread={handleMarkProjectUnread}
+ onRemoveProject={handleDeleteProjectRequest}
/>
diff --git a/apps/staged/src/lib/features/projects/ProjectsList.svelte b/apps/staged/src/lib/features/projects/ProjectsList.svelte
index 61b96630..e3a47cc5 100644
--- a/apps/staged/src/lib/features/projects/ProjectsList.svelte
+++ b/apps/staged/src/lib/features/projects/ProjectsList.svelte
@@ -26,11 +26,14 @@
import { selectProject } from '../layout/navigation.svelte';
import NewProjectModal from './NewProjectModal.svelte';
import ProjectsSidebar from './ProjectsSidebar.svelte';
+ import ProjectContextMenu from './ProjectContextMenu.svelte';
import { getProjectStatus } from './projectStatus';
import SplashScreen from './SplashScreen.svelte';
import Spinner from '../../shared/Spinner.svelte';
import SineWave from '../../shared/SineWave.svelte';
import RepoLabel from '../../shared/RepoLabel.svelte';
+ import ConfirmDialog from '../../shared/ConfirmDialog.svelte';
+ import { alerts } from '../../shared/alerts.svelte';
import { setProjects } from './projectsSidebarState.svelte';
import {
@@ -41,6 +44,7 @@
import { darkMode } from '../../stores/isDark.svelte';
import { repoBadgeStore } from '../../stores/repoBadges.svelte';
import { badgeBg, badgeFg, badgeBgHover } from '../../shared/badgeColors';
+ import { canDeleteProjectWithoutConfirmation } from './projectDeleteSafety';
type FilterKind = 'unread' | 'running' | { repo: string; subpath: string };
@@ -51,6 +55,8 @@
let showNewProjectModal = $state(false);
let isCommandKeyHeld = $state(false);
let deletingProjectNames = $state