Skip to content

fix(ui): persist group create/rename/move even during a reload window (fixes #1539)#1573

Open
drmzperx wants to merge 3 commits into
asheshgoplani:mainfrom
drmzperx:fix/1539-group-persist-during-reload
Open

fix(ui): persist group create/rename/move even during a reload window (fixes #1539)#1573
drmzperx wants to merge 3 commits into
asheshgoplani:mainfrom
drmzperx:fix/1539-group-persist-during-reload

Conversation

@drmzperx

@drmzperx drmzperx commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Creating a root group with g (or renaming/moving one) only worked every second try — the group appeared in memory but vanished on the next storage-watcher reload. Fixes #1539.

Root cause

The GroupDialogCreate/GroupDialogRename/GroupDialogMove handlers persisted via saveInstances(), which is a no-op while isReloading (and also aborts on the mtime external-change check). Every successful save triggers a storage-watcher reload; if the next group mutation lands inside that reload window, its save is silently skipped, and when the reload rebuilds groupTree from disk the mutation vanishes. That produces exactly the alternating "works every second try" behavior reported in #1539.

Session creation already uses forceSaveInstances() for this exact reason (the was losing groups! comment in sessionCreatedMsg), and session renames are protected by the pendingTitleChanges replay — group-structure mutations had neither.

Fix

Switch the three group-structure mutations (create, rename, move) to forceSaveInstances() so a user-initiated group change persists regardless of the reload flag.

Tradeoff, made deliberately: forceSaveInstances() also bypasses the mtime external-change guard, so a group save landing in a reload window could overwrite a concurrent external write. This matches the existing precedent for session creation; if the codebase later moves group mutations to targeted UPDATEs (like the title-persist path), these sites are the candidates.

Tests

internal/ui/group_persist_during_reload_test.go drives the real handleGroupDialogKey handler with isReloading=true against real SQLite storage under an isolated $HOME, and asserts the created/renamed group survives a LoadWithGroups round-trip. Both tests fail on main and pass with the fix.

go test ./internal/ui/ -run 'PersistsDuringReload|GroupDialog' -count=1
ok  	github.com/asheshgoplani/agent-deck/internal/ui	0.162s

Summary by CodeRabbit

  • Bug Fixes
    • Group creation, renaming, and moving now reliably save during reloads.
    • Fixed an issue where changes to groups could be lost or only appear after retrying an action.
    • Added regression coverage to ensure group updates remain persisted after reload-related state changes.

drmzperx added 2 commits July 6, 2026 16:45
Creating a root group with 'g' or renaming one with 'r' only worked
every second try. The GroupDialog Create/Rename/Move handlers persisted
via saveInstances(), which is a no-op while isReloading (and aborts on
the mtime external-change check). Each successful group save triggers a
storage-watcher reload; if the next group mutation lands in that window
its save is skipped, and when the reload rebuilds groupTree from disk
the mutation vanishes — the alternating 'every second try' behavior.

Session creation already uses forceSaveInstances() for exactly this
reason ('was losing groups!'), and session renames get pendingTitleChanges
replay; group mutations had neither. Switch the three group-structure
mutations to forceSaveInstances() so they persist regardless of the
reload flag.

Regression tests drive the real handler with isReloading=true and assert
the group survives a reload round-trip through real SQLite storage.
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@drmzperx, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 94bac924-e33a-4c93-9628-0de3434465e8

📥 Commits

Reviewing files that changed from the base of the PR and between 0e4caab and 15300af.

📒 Files selected for processing (1)
  • internal/ui/group_persist_during_reload_test.go
📝 Walkthrough

Walkthrough

Group dialog handlers for create, rename, and move operations in home.go now call forceSaveInstances() instead of saveInstances(), ensuring persistence occurs even during storage-watcher reload windows. A new test file adds regression coverage using a real SQLite-backed Home instance to verify group create and rename persistence during reload.

Changes

Group persistence during reload

Layer / File(s) Summary
Force persistence for group create, rename, and move
internal/ui/home.go
GroupDialogCreate, GroupDialogRename, and GroupDialogMove now call h.forceSaveInstances() instead of h.saveInstances() so group mutations persist even while h.isReloading is true.
Regression tests for reload-window persistence
internal/ui/group_persist_during_reload_test.go
Adds testHomeWithStorage and groupPersisted test helpers, plus TestGroupCreatePersistsDuringReload and TestGroupRenamePersistsDuringReload, which set h.isReloading = true and verify group create/rename persistence against real SQLite storage.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant GroupDialog
  participant Home
  participant Storage
  User->>GroupDialog: create/rename/move group
  GroupDialog->>Home: apply group mutation
  Home->>Home: check isReloading
  Home->>Storage: forceSaveInstances()
  Storage-->>Home: persisted group data
Loading

Related issues: #1539 (Create Group requiring two attempts to appear, caused by saveInstances being skipped during a reload window)

Suggested labels: review_needed_junior_swe, review_depth_standard

Suggested reviewers: asheshgoplani

🐰 A group once vanished, then reappeared twice,
Skipped by a reload—not very nice.
Now forceSaveInstances locks it in tight,
One try, one group, persisted right.
Tests confirm it, hop hop, all's well tonight!

🚥 Pre-merge checks | ✅ 5 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Remote_parity ⚠️ Warning New internal/ui regression tests cover group create/rename, but neither file adds the required RemoteSession note nor a t.Skip documenting why it’s out of scope. Add an explicit comment or t.Skip in the new test file stating RemoteSession isn’t applicable to these local group flows.
Test_coverage_per_surface ⚠️ Warning The PR adds reload-window tests for create and rename, but the changed GroupDialogMove path has no equivalent regression coverage. Add a test that exercises GroupDialogMove with isReloading=true and verifies the move survives a persisted reload round-trip.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title uses Conventional Commits format and accurately describes the group persistence fix.
Linked Issues check ✅ Passed The changes address #1539 by forcing group creation to persist during reload windows and adding regression coverage.
Out of Scope Changes check ✅ Passed The rename and move persistence changes are closely related to the same reload-window bug and stay within scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/ui/group_persist_during_reload_test.go (1)

67-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a regression test for GroupDialogMove.

The PR also switches GroupDialogMove to forceSaveInstances(), but only create and rename get reload-window regression tests here. Adding a matching TestGroupMovePersistsDuringReload (seed a session + two groups, move during isReloading, verify via groupPersisted/instance group path) would close the coverage gap for the third changed call site.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/ui/group_persist_during_reload_test.go` around lines 67 - 108, Add a
regression test for the `GroupDialogMove` persistence change, since only create
and rename are covered today. In
`internal/ui/group_persist_during_reload_test.go`, add a
`TestGroupMovePersistsDuringReload` that seeds the necessary session/groups with
`testHomeWithStorage` and `forceSaveInstances`, then sets `h.isReloading = true`
and drives `handleGroupDialogKey` through the move flow. Verify the move is
persisted using `groupPersisted` and the instance group path, so the third
`forceSaveInstances()` call site is covered during a reload window.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/ui/group_persist_during_reload_test.go`:
- Around line 67-108: The new tests in TestGroupCreatePersistsDuringReload and
TestGroupRenamePersistsDuringReload need an explicit RemoteSession
acknowledgement or a skip note to satisfy the internal/ui test guideline. Update
this test file by either adding coverage/verification for RemoteSession behavior
in the group persistence flow, or inserting a clear t.Skip in the relevant
test(s) explaining that groupTree and session.Instance here are local-only and
RemoteSession is out of scope. Keep the note close to the existing test setup in
testHomeWithStorage, handleGroupDialogKey, and groupPersisted so future readers
can see why RemoteSession is intentionally not exercised.

---

Nitpick comments:
In `@internal/ui/group_persist_during_reload_test.go`:
- Around line 67-108: Add a regression test for the `GroupDialogMove`
persistence change, since only create and rename are covered today. In
`internal/ui/group_persist_during_reload_test.go`, add a
`TestGroupMovePersistsDuringReload` that seeds the necessary session/groups with
`testHomeWithStorage` and `forceSaveInstances`, then sets `h.isReloading = true`
and drives `handleGroupDialogKey` through the move flow. Verify the move is
persisted using `groupPersisted` and the instance group path, so the third
`forceSaveInstances()` call site is covered during a reload window.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 687e3581-5f7c-47eb-90d9-2f6915222a9e

📥 Commits

Reviewing files that changed from the base of the PR and between f70f19e and 0e4caab.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !**/*.md, !CHANGELOG.md
📒 Files selected for processing (2)
  • internal/ui/group_persist_during_reload_test.go
  • internal/ui/home.go

Comment on lines +67 to +108
func TestGroupCreatePersistsDuringReload(t *testing.T) {
h, storage := testHomeWithStorage(t, "_group_persist_create")

// Simulate a storage-watcher reload in flight — the window in which the old
// saveInstances() silently dropped the write.
h.isReloading = true

// Drive the real create flow: create-mode dialog + Enter.
h.groupDialog.Show()
h.groupDialog.nameInput.SetValue("alpha")
h.handleGroupDialogKey(tea.KeyMsg{Type: tea.KeyEnter})

if !groupPersisted(t, storage, "alpha") {
t.Error(`group "alpha" created during a reload window was not persisted; ` +
`create must use forceSaveInstances (saveInstances is skipped while isReloading)`)
}
}

func TestGroupRenamePersistsDuringReload(t *testing.T) {
h, storage := testHomeWithStorage(t, "_group_persist_rename")

// Seed and persist a group while NOT reloading.
h.groupTree.CreateGroup("old")
h.forceSaveInstances()
if !groupPersisted(t, storage, "old") {
t.Fatal("precondition: seed group 'old' should be persisted")
}

// Now rename it during a reload window.
h.isReloading = true
h.groupDialog.ShowRename("old", "old")
h.groupDialog.nameInput.SetValue("new")
h.handleGroupDialogKey(tea.KeyMsg{Type: tea.KeyEnter})

if groupPersisted(t, storage, "old") {
t.Error(`old group name still present after rename during reload window`)
}
if !groupPersisted(t, storage, "new") {
t.Error(`renamed group "new" was not persisted during reload window; ` +
`rename must use forceSaveInstances`)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Missing explicit RemoteSession note/t.Skip per path guideline.

These tests exercise group create/rename during a reload window but don't mention RemoteSession or include a t.Skip explaining why it's out of scope. Groups here operate purely on local session.Instance data (groupTree), so RemoteSession is likely structurally inapplicable — similar to the fork-dispatch precedent — but the guideline calls for that to be stated explicitly.

As per path instructions, internal/ui/**/*_test.go files touching TUI behavior must "verify it also handles RemoteSession or include an explicit t.Skip documenting why."

📝 Suggested doc note
 func TestGroupCreatePersistsDuringReload(t *testing.T) {
+	// RemoteSession items are read-only views with no local Instance/groupTree
+	// membership, so they are not applicable to group create/rename flows.
 	h, storage := testHomeWithStorage(t, "_group_persist_create")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestGroupCreatePersistsDuringReload(t *testing.T) {
h, storage := testHomeWithStorage(t, "_group_persist_create")
// Simulate a storage-watcher reload in flight — the window in which the old
// saveInstances() silently dropped the write.
h.isReloading = true
// Drive the real create flow: create-mode dialog + Enter.
h.groupDialog.Show()
h.groupDialog.nameInput.SetValue("alpha")
h.handleGroupDialogKey(tea.KeyMsg{Type: tea.KeyEnter})
if !groupPersisted(t, storage, "alpha") {
t.Error(`group "alpha" created during a reload window was not persisted; ` +
`create must use forceSaveInstances (saveInstances is skipped while isReloading)`)
}
}
func TestGroupRenamePersistsDuringReload(t *testing.T) {
h, storage := testHomeWithStorage(t, "_group_persist_rename")
// Seed and persist a group while NOT reloading.
h.groupTree.CreateGroup("old")
h.forceSaveInstances()
if !groupPersisted(t, storage, "old") {
t.Fatal("precondition: seed group 'old' should be persisted")
}
// Now rename it during a reload window.
h.isReloading = true
h.groupDialog.ShowRename("old", "old")
h.groupDialog.nameInput.SetValue("new")
h.handleGroupDialogKey(tea.KeyMsg{Type: tea.KeyEnter})
if groupPersisted(t, storage, "old") {
t.Error(`old group name still present after rename during reload window`)
}
if !groupPersisted(t, storage, "new") {
t.Error(`renamed group "new" was not persisted during reload window; ` +
`rename must use forceSaveInstances`)
}
}
func TestGroupCreatePersistsDuringReload(t *testing.T) {
// RemoteSession items are read-only views with no local Instance/groupTree
// membership, so they are not applicable to group create/rename flows.
h, storage := testHomeWithStorage(t, "_group_persist_create")
// Simulate a storage-watcher reload in flight — the window in which the old
// saveInstances() silently dropped the write.
h.isReloading = true
// Drive the real create flow: create-mode dialog + Enter.
h.groupDialog.Show()
h.groupDialog.nameInput.SetValue("alpha")
h.handleGroupDialogKey(tea.KeyMsg{Type: tea.KeyEnter})
if !groupPersisted(t, storage, "alpha") {
t.Error(`group "alpha" created during a reload window was not persisted; ` +
`create must use forceSaveInstances (saveInstances is skipped while isReloading)`)
}
}
func TestGroupRenamePersistsDuringReload(t *testing.T) {
h, storage := testHomeWithStorage(t, "_group_persist_rename")
// Seed and persist a group while NOT reloading.
h.groupTree.CreateGroup("old")
h.forceSaveInstances()
if !groupPersisted(t, storage, "old") {
t.Fatal("precondition: seed group 'old' should be persisted")
}
// Now rename it during a reload window.
h.isReloading = true
h.groupDialog.ShowRename("old", "old")
h.groupDialog.nameInput.SetValue("new")
h.handleGroupDialogKey(tea.KeyMsg{Type: tea.KeyEnter})
if groupPersisted(t, storage, "old") {
t.Error(`old group name still present after rename during reload window`)
}
if !groupPersisted(t, storage, "new") {
t.Error(`renamed group "new" was not persisted during reload window; ` +
`rename must use forceSaveInstances`)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/ui/group_persist_during_reload_test.go` around lines 67 - 108, The
new tests in TestGroupCreatePersistsDuringReload and
TestGroupRenamePersistsDuringReload need an explicit RemoteSession
acknowledgement or a skip note to satisfy the internal/ui test guideline. Update
this test file by either adding coverage/verification for RemoteSession behavior
in the group persistence flow, or inserting a clear t.Skip in the relevant
test(s) explaining that groupTree and session.Instance here are local-only and
RemoteSession is out of scope. Keep the note close to the existing test setup in
testHomeWithStorage, handleGroupDialogKey, and groupPersisted so future readers
can see why RemoteSession is intentionally not exercised.

Source: Path instructions

The fix switched all three group mutations to forceSaveInstances(), but
only create and rename had reload-window regression tests (CodeRabbit
nit on asheshgoplani#1573). Add TestGroupMovePersistsDuringReload: move the seed
session into a destination group with isReloading=true and assert the
session's GroupPath survives a storage round-trip. Verified it fails
when the Move handler is reverted to saveInstances().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create Group has to be done twice for the group to appear in the list

1 participant