Skip to content

feat(board): edit, reorder, and confirm-delete projects#3513

Merged
dgageot merged 2 commits into
docker:mainfrom
dgageot:board-manage-projects
Jul 7, 2026
Merged

feat(board): edit, reorder, and confirm-delete projects#3513
dgageot merged 2 commits into
docker:mainfrom
dgageot:board-manage-projects

Conversation

@dgageot

@dgageot dgageot commented Jul 7, 2026

Copy link
Copy Markdown
Member

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 to Store.RenameProject so 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) via App.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 same ConfirmKeyMap as 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.md is updated to document the new interactions.

dgageot added 2 commits July 7, 2026 16:34
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
@dgageot dgageot requested a review from a team as a code owner July 7, 2026 14:48

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pkg/board/app.go
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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).

Comment thread pkg/board/tui/tui.go
if d, ok := m.dialog.(*projectsDialog); ok {
d.setProjects(m.projects)
if d.mode == projectsEditing {
d.setProjects(m.projects)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@aheritier aheritier added area/tui For features/issues/fixes related to the TUI kind/feat PR adds a new feature (maps to feat:). Use on PRs only. labels Jul 7, 2026
@dgageot dgageot merged commit e1c9203 into docker:main Jul 7, 2026
15 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/tui For features/issues/fixes related to the TUI kind/feat PR adds a new feature (maps to feat:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants