feat(prompts): add folders and reordering to prompts#2506
Conversation
Greptile SummaryThis PR adds folder organization and drag-to-reorder support to the prompt library, touching the main-process service, IPC controller, the library view, the prompt/folder modals, the context popover, and the shared schema.
Confidence Score: 3/5The feature works correctly in the happy path, but the two-write pattern in updateState can permanently lose folder assignments if the process crashes or if the second KV write silently fails. The prompt/folder library is the only persisted data path changed here. Both writes go through the same SQLite database, so wrapping them in a db.transaction() would fully close the gap. Until that is done, every folder create/rename/delete/reorder operation risks leaving prompts with orphaned folderId values that are silently stripped on the next boot. apps/emdash-desktop/src/main/core/prompt-library/service.ts — the updateState method needs both writes in a single transaction before this is safe to ship.
|
| Filename | Overview |
|---|---|
| apps/emdash-desktop/src/main/core/prompt-library/service.ts | Adds folder read/write and sanitization logic; introduces a non-atomic two-write pattern for prompts+folders that can silently lose folder assignments on crash or write error |
| apps/emdash-desktop/src/renderer/features/library/prompts/use-prompt-library.ts | Migrates hook to PromptLibraryState; adds a reorder helper that pre-writes the cache before mutation, making optimistic rollback a no-op (recovery relies solely on invalidateQueries) |
| apps/emdash-desktop/src/renderer/features/library/prompts/prompt-library-view.tsx | Large refactor adding dnd-kit drag-and-drop reordering and folder sections; drag state management (draftPrompts, pendingOrder, dragState) is well-structured with correct cross-section preview logic |
| apps/emdash-desktop/src/renderer/features/tasks/conversations/add-context-popover.tsx | Adds folder navigation to the context popover with a keepOpenUntilRef timing guard to suppress input echo on folder selection; logic is sound |
| apps/emdash-desktop/src/shared/prompt-library.ts | Adds folderId to prompt schema, new folder schema/type, and PromptLibraryState wrapper type; clean addition with proper Zod validation |
| apps/emdash-desktop/src/renderer/features/library/prompts/prompt-folder-modal.tsx | New modal for creating/renaming folders; duplicate detection correctly handles case-insensitive comparison and excludes the current folder name on rename |
| apps/emdash-desktop/src/renderer/features/library/prompts/prompt-modal.tsx | Adds optional folder select field; NO_FOLDER sentinel correctly prevents UUID collision and the select is only rendered when folders exist |
| apps/emdash-desktop/src/main/core/prompt-library/controller.ts | Straightforward update to use new getState/updateState signatures; no logic changes |
Sequence Diagram
sequenceDiagram
participant UI as PromptLibraryView
participant Hook as usePromptLibrary
participant QC as QueryClient
participant IPC as RPC (IPC)
participant Svc as PromptLibraryService
participant KV as KV Store (SQLite)
Note over UI: User drag-drops prompt
UI->>Hook: "reorder({ prompts, folders })"
Hook->>QC: setQueryData(newState) [immediate]
Hook->>Hook: updateMutation.mutate(newState)
Hook->>QC: cancelQueries
Hook->>QC: "getQueryData -> previousState (= newState)"
Hook->>QC: setQueryData(newState)
Hook->>IPC: rpc.promptLibrary.update(state)
IPC->>Svc: updateState(state)
Svc->>KV: set('prompts', sanitizedPrompts)
KV-->>Svc: ok
Svc->>KV: set('folders', folders)
KV-->>Svc: ok
Svc-->>IPC: void
IPC-->>Hook: resolved
Hook->>QC: "invalidateQueries -> refetch"
QC->>IPC: rpc.promptLibrary.get()
IPC->>Svc: getState()
Svc->>KV: get('prompts') + get('folders')
KV-->>Svc: data
Svc->>Svc: sanitizePrompts(prompts, folders)
Svc-->>IPC: PromptLibraryState
IPC-->>QC: fresh state
QC-->>UI: re-render
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
apps/emdash-desktop/src/main/core/prompt-library/service.ts:130-135
**Non-atomic writes can silently lose folder assignments**
`updateState` writes prompts and folders as two independent SQLite `INSERT ... ON CONFLICT UPDATE` statements. `KV.set` also swallows write errors rather than rethrowing, so if the prompts write succeeds but the folders write fails (or the process crashes in between), the persisted state is inconsistent. On the next boot, `getState` reads the updated prompts (containing a `folderId` for the just-created folder) alongside the stale folders list (which doesn't include that folder yet), and `sanitizePrompts` silently strips all those `folderId` values — permanently losing the user's folder assignments.
Since both keys live in the same SQLite database through the Drizzle `db` instance, wrapping the two writes in a single `db.transaction(...)` call would make this atomic at zero cost.
### Issue 2 of 2
apps/emdash-desktop/src/renderer/features/library/prompts/use-prompt-library.ts:42-45
**`reorder` pre-writes the cache, so `onMutate` captures the new state as `previousState`**
When `reorder` is called it immediately runs `queryClient.setQueryData(promptLibraryQueryKey, state)` before kicking off the mutation. When the mutation's `onMutate` then fires, `previousState = queryClient.getQueryData(...)` reads back the *already-updated* value — not the original. If the IPC call subsequently fails, `onError` rolls the cache back to that same new-order snapshot, which is a no-op. The only recovery is the `invalidateQueries` in `onSettled`, which will eventually restore the server state, but during that window the UI shows the (failed) new order as if it succeeded.
The comment acknowledges this trade-off ("Errors are recovered by the mutation's invalidate"), so the intent is clear. Consider capturing the true prior state before the pre-write if a more accurate rollback is ever needed.
Reviews (1): Last reviewed commit: "feat(prompts): add folders and reorderin..." | Re-trigger Greptile
Description
Screenshot/Recording (if applicable)
https://streamable.com/q96hk4
Checklist
messages and, when possible, the PR title