fix(tabs): report an edit dropped behind an in-flight save, respect the group cap (#525 landing)

- createEditCoordinator's finally block rebases the edits queued during a write; one that the write's 409 made inapplicable was dropped with no toast. It is now reported once, like the main loop and adoptExternal do (found by the PR bot's re-review; regression test fails without it).
- At the 32-group server cap the row and group menus no longer offer a new group, which could only fail with an untranslated 'group limit reached'. MAX_GROUPS is exported from tab-layout-browser.js.
- CLAUDE.md names the pagehide keepalive as the one deliberate exception to 'never PUT the layout outside the coordinator'.
- The Dashboard wiki page describes tab groups in the vertical rail row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-05 19:51:59 +02:00
parent 6f88e40b77
commit 737a2527d6
6 changed files with 60 additions and 5 deletions
+8 -2
View File
@@ -6694,7 +6694,8 @@ class CodemanApp {
actions.push({ label: `Move to "${group.name}"`, run: () => this.moveTabRef(ref, group.id) });
}
if (location.groupId !== null) actions.push({ label: 'Move to Ungrouped', run: () => this.moveTabRef(ref, null) });
actions.push({ label: 'Move to new group', run: () => this.createTabGroup({ ref }) });
// At the server's group cap a new group can only fail, so it is not offered.
if (this._canCreateTabGroup()) actions.push({ label: 'Move to new group', run: () => this.createTabGroup({ ref }) });
return actions;
}
@@ -6728,13 +6729,18 @@ class CodemanApp {
item?.focus();
}
_canCreateTabGroup() {
const max = window.CodemanTabLayout?.MAX_GROUPS;
return !max || (this.tabLayout?.groups?.length || 0) < max;
}
openTabGroupMenu(event, groupId) {
const groups = this.tabLayout?.groups || [];
const index = groups.findIndex((group) => group.id === groupId);
if (index < 0) return false;
return this._openTabLayoutMenu(event, `group:${groupId}`, 'Group actions', [
{ label: 'Rename group', run: () => this.startTabGroupRename(groupId) },
{ label: 'New group', run: () => this.createTabGroup({ index: index + 1 }) },
...(this._canCreateTabGroup() ? [{ label: 'New group', run: () => this.createTabGroup({ index: index + 1 }) }] : []),
...(index > 0 ? [{ label: 'Move group up', run: () => this.moveTabGroup(groupId, -1) }] : []),
...(index < groups.length - 1 ? [{ label: 'Move group down', run: () => this.moveTabGroup(groupId, 1) }] : []),
{ label: 'Delete group', className: 'danger', run: () => this.deleteTabGroup(groupId) },
+6
View File
@@ -658,6 +658,11 @@
writing = false;
if (!disposed) {
const rebased = replayOperations(authoritative, pending);
// Edits made while the write was in flight are rebased here, so one the
// conflict made inapplicable is dropped here too, and says so (once).
if (rebased.dropped && !failed && !reportedDrop) {
report('Tab groups changed elsewhere; part of your edit no longer applies.');
}
pending = rebased.kept;
optimistic = rebased.layout;
publish({ authoritative: true });
@@ -740,5 +745,6 @@
createLoadCoordinator,
loadCollapsedGroupIds,
saveCollapsedGroupIds,
MAX_GROUPS,
};
})(typeof window !== 'undefined' ? window : globalThis);