feat(storage): add task worktree storage management#2686
Open
rabanspiegel wants to merge 1 commit into
Open
Conversation
Contributor
Greptile SummaryThis PR adds a Storage settings area for task worktree cleanup. The main changes are:
Confidence Score: 4/5The batch task deletion event path needs a fix before merging.
apps/emdash-desktop/src/main/core/tasks/task-service.ts
|
| Filename | Overview |
|---|---|
| apps/emdash-desktop/src/main/core/storage/storage-service.ts | Adds the main storage service for querying task/workspace rows, measuring local worktrees, grouping results, and delegating bulk deletion through the task service. |
| apps/emdash-desktop/src/main/core/tasks/task-service.ts | Adds task deletion event emission, but the batch delete path can skip notifications for successful deletes when another delete fails. |
| apps/emdash-desktop/src/main/core/tasks/operations/task-lifecycle-utils.ts | Adds local owned-worktree fallback removal with local/remote checks, project-root refusal, sibling-task checks, and git worktree pruning. |
| apps/emdash-desktop/src/renderer/features/settings/components/StorageSettingsPage.tsx | Adds the Storage settings UI for scanning usage, selecting tasks, confirming bulk deletion, and showing per-operation feedback. |
| apps/emdash-desktop/src/renderer/features/tasks/stores/task-manager.ts | Subscribes task managers to task deletion events and adds delete failure toast feedback. |
| packages/core/src/storage/measurement.ts | Adds recursive filesystem measurement with apparent and reclaimable byte accounting plus scan errors. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI as Storage Settings UI
participant RPC as storage RPC
participant Service as Storage Service
participant Tasks as Task Service
participant DB as Task/Workspace DB
participant FS as Worktree Filesystem
UI->>RPC: listTaskStorageUsage()
RPC->>Service: listTaskStorageUsage(projectId?)
Service->>DB: read tasks, projects, workspaces
Service->>FS: measure local worktree paths
Service-->>UI: grouped usage
UI->>RPC: deleteTasks(taskIds)
RPC->>Service: deleteStorageTasks(taskIds)
loop each selected task
Service->>Tasks: "deleteTask(projectId, taskId, deleteWorktree=true)"
Tasks->>DB: remove task row and cleanup state
Tasks->>FS: remove owned local worktree when unused
Tasks-->>UI: taskDeleted event
end
Service-->>UI: per-task delete result
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI as Storage Settings UI
participant RPC as storage RPC
participant Service as Storage Service
participant Tasks as Task Service
participant DB as Task/Workspace DB
participant FS as Worktree Filesystem
UI->>RPC: listTaskStorageUsage()
RPC->>Service: listTaskStorageUsage(projectId?)
Service->>DB: read tasks, projects, workspaces
Service->>FS: measure local worktree paths
Service-->>UI: grouped usage
UI->>RPC: deleteTasks(taskIds)
RPC->>Service: deleteStorageTasks(taskIds)
loop each selected task
Service->>Tasks: "deleteTask(projectId, taskId, deleteWorktree=true)"
Tasks->>DB: remove task row and cleanup state
Tasks->>FS: remove owned local worktree when unused
Tasks-->>UI: taskDeleted event
end
Service-->>UI: per-task delete result
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
apps/emdash-desktop/src/main/core/tasks/task-service.ts:182-189
**Partial Deletes Stay Visible**
When one task in `deleteTasks` fails after another has already been deleted, `Promise.all` rejects before this notification loop runs. No `taskDeletedChannel` event is emitted for the successful deletion, so the task list catch path can restore every optimistic row and leave a deleted task visible in the renderer.
```suggestion
async deleteTasks(
projectId: string,
taskIds: string[],
options?: DeleteTaskOptions
): Promise<void> {
const results = await Promise.allSettled(taskIds.map((id) => deleteTask(projectId, id, options)));
const failures: unknown[] = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
this.notifyTaskDeleted(taskIds[index]!, projectId);
} else {
failures.push(result.reason);
}
});
if (failures.length > 0) {
throw failures[0];
}
}
```
Reviews (1): Last reviewed commit: "feat(storage): add task worktree storage..." | Re-trigger Greptile
Comment on lines
182
to
189
| @@ -176,7 +185,7 @@ export class TaskService implements Hookable<TaskLifecycleHooks> { | |||
| options?: DeleteTaskOptions | |||
| ): Promise<void> { | |||
| await Promise.all(taskIds.map((id) => deleteTask(projectId, id, options))); | |||
| taskIds.forEach((id) => this._hooks.callHookBackground('task:deleted', id, projectId)); | |||
| taskIds.forEach((id) => this.notifyTaskDeleted(id, projectId)); | |||
| } | |||
Contributor
There was a problem hiding this comment.
When one task in deleteTasks fails after another has already been deleted, Promise.all rejects before this notification loop runs. No taskDeletedChannel event is emitted for the successful deletion, so the task list catch path can restore every optimistic row and leave a deleted task visible in the renderer.
Suggested change
| async deleteTasks( | |
| projectId: string, | |
| taskIds: string[], | |
| options?: DeleteTaskOptions | |
| ): Promise<void> { | |
| const results = await Promise.allSettled(taskIds.map((id) => deleteTask(projectId, id, options))); | |
| const failures: unknown[] = []; | |
| results.forEach((result, index) => { | |
| if (result.status === 'fulfilled') { | |
| this.notifyTaskDeleted(taskIds[index]!, projectId); | |
| } else { | |
| failures.push(result.reason); | |
| } | |
| }); | |
| if (failures.length > 0) { | |
| throw failures[0]; | |
| } | |
| } |
Context Used: CLAUDE.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/emdash-desktop/src/main/core/tasks/task-service.ts
Line: 182-189
Comment:
**Partial Deletes Stay Visible**
When one task in `deleteTasks` fails after another has already been deleted, `Promise.all` rejects before this notification loop runs. No `taskDeletedChannel` event is emitted for the successful deletion, so the task list catch path can restore every optimistic row and leave a deleted task visible in the renderer.
```suggestion
async deleteTasks(
projectId: string,
taskIds: string[],
options?: DeleteTaskOptions
): Promise<void> {
const results = await Promise.allSettled(taskIds.map((id) => deleteTask(projectId, id, options)));
const failures: unknown[] = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
this.notifyTaskDeleted(taskIds[index]!, projectId);
} else {
failures.push(result.reason);
}
});
if (failures.length > 0) {
throw failures[0];
}
}
```
**Context Used:** CLAUDE.md ([source](https://app.greptile.com/emdash/github/generalaction/emdash/-/custom-context?memory=39946a11-2903-4cb1-9b64-bdbe746b20d1))
How can I resolve this? If you propose a fix, please make it concise.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a Storage settings page for reviewing task worktree disk usage and deleting stale tasks from one place.
This version:
Why
Worktrees can accumulate and take meaningful disk space, especially across old tasks. Users need a direct way to see which tasks are taking space and remove them without manually inspecting ~/emdash/worktrees.
This also gives us a clean foundation for future storage work without putting disk-scanning logic directly in the renderer or duplicating task deletion behavior.
Implementation
Adds a small @emdash/core/storage package for filesystem measurement. This keeps the disk-size logic independent from Electron/main-process UI concerns and makes it easier to reuse later in a workspace-server direction.
Adds a typed main-process storage RPC namespace:
The Storage service queries tasks/workspaces, measures local worktree usage, and returns Storage-specific view data to the renderer.
Deletion intentionally reuses the canonical task deletion path:
So Storage does not directly delete task DB rows, view state, telemetry, or task events. That stays owned by the task lifecycle code.
The task deletion path now also has a local owned-path fallback: if the mounted project provider cannot remove the worktree, deletion can still remove the DB-owned local worktree path, while refusing to delete the project root.
UI
Adds Settings -> Storage with:
Regular sidebar/task-list deletes stay quiet on success, but now show an error toast if deletion fails. Storage keeps delete feedback because bulk deletion can partially fail.
Non-goals
This does not add:
Testing