feat(board): edit, reorder, and confirm-delete projects#3513
Conversation
Add three new operations to the projects dialog: - Edit: press `e` or `enter` to reopen the add-form pre-filled with the selected project's name, path, and agent (App.UpdateProject). - Reorder: shift+↑/↓ (or K/J) move the selected project up/down (App.MoveProject); the cursor follows the item. - Delete confirmation: pressing `x`/`d`/backspace now shows a confirmation prompt before removing the project. Tests cover App.UpdateProject and App.MoveProject, and a new dialogs_test.go exercises the dialog state machine. Assisted-By: Claude
- fix data loss: ctrl+o from add form now branches on inputs!=nil so typed name/agent survive the picker round-trip; esc clears inputs - moveProjectMsg uses refreshProjects to avoid stomping an open edit form or delete confirmation with a forced list-mode reset - saveProject runs add/update in a tea.Cmd (git subprocess off UI loop); projectSavedMsg handles lastProject rename follow-through - RenameProject rewrites card.Project on rename so cards keep their accent color/header; UpdateProject calls it when the name changes - validateProject trims name/agent; duplicate checks use stored (trimmed) name Assisted-By: Claude
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🔴 CRITICAL
Two bugs introduced by this PR were confirmed: one is a high-severity data consistency issue in UpdateProject and one is a medium-severity race in the async save/edit flow in the TUI. See inline comments for details.
| if stored.Name != oldName { | ||
| // Keep existing cards attached to their project across the rename | ||
| // (their accent color and header stay consistent). | ||
| if err := a.store.RenameProject(oldName, stored.Name); err != nil { |
There was a problem hiding this comment.
[HIGH] UpdateProject leaves config and card store inconsistent when RenameProject fails
When the project name changes, saveConfigLocked() writes the new name to the config file first, and then store.RenameProject() migrates existing cards. If store.RenameProject() returns an error, the function propagates it but does not roll back the config save. The result is a permanent split-brain state: the config shows the project under its new name while every card in the store still references the old name. Cards become orphaned and cannot be displayed under either name reliably.
Suggested fix: either (a) rename in the store before calling saveConfigLocked() so that a config-save failure leaves the store unmodified, or (b) attempt a compensating rename back to oldName in the error path (though that can also fail, making option (a) safer).
| if d, ok := m.dialog.(*projectsDialog); ok { | ||
| d.setProjects(m.projects) | ||
| if d.mode == projectsEditing { | ||
| d.setProjects(m.projects) |
There was a problem hiding this comment.
[MEDIUM] projectSavedMsg may discard a newly-opened edit form if user navigates away and back before the async save completes
When the user presses Enter in the edit form, updateEditing emits a submitProjectMsg but does not reset d.mode — the dialog stays in projectsEditing while the save goroutine runs. If the user then presses Esc (returning to list mode) and immediately presses 'e' again to edit a different project, the dialog is once again in projectsEditing when the goroutine's projectSavedMsg arrives. The handler's check if d.mode == projectsEditing cannot distinguish "still in the original form" from "user already opened a new form" — it calls d.setProjects(m.projects) in both cases, which forcibly resets the mode to projectsList and sets d.inputs = nil, silently discarding all the user's in-progress input on the second project.
Suggested fix: record the name of the project being saved (e.g. store d.savingOldName when enter is pressed) and in the projectSavedMsg handler only call setProjects when d.editing == msg.oldName, so that a completed save for a stale form does not evict a subsequently opened one.
The board's project management was read-only after initial creation: users could add and delete projects but couldn't rename them, change their order, or confirm destructive deletes. This adds the remaining lifecycle operations and hardens several edge cases surfaced during code review.
Pressing e or enter on a project in the projects dialog now opens a pre-filled edit form. Saving calls
App.UpdateProject, which persists the new name and propagates it toStore.RenameProjectso existing cards stay associated with the renamed project and the new-card dialog's last-used project tracks the change. Projects can be reordered with shift+↑/↓ (or J/K) viaApp.MoveProject, clamped at list boundaries; order drives both accent-color assignment and the project selector in the new-card form. Deletion now requires confirmation using the sameConfirmKeyMapas card deletion — y or enter confirms, n or esc cancels — so a stray keystroke can't silently destroy a project and all its cards.The second commit addresses findings from code review: a ctrl+o browse round-trip no longer discards a form in progress; async move/save results are gated so they can't overwrite an open form; validation (the git subprocess that checks agent names) runs off the UI goroutine; and project names and agent identifiers are trimmed consistently before storage. Tests were added at the engine level (
TestUpdateProject,TestMoveProject) and at the dialog level covering confirm-delete, edit prefill, reorder event emission, and the browse round-trip regression.docs/features/board/index.mdis updated to document the new interactions.