fix(tabs): Follow parent again keeps a Focus pin (#528/#582 integration)

#528's browser followParent operation rebuilt the target ref as
{ kind, id } to drop its manual placement, which also dropped #582's
`focus` flag. The browser PUTs whole layouts, so the pin was erased on the
next save. The operation now copies the ref and removes only `placement`.
#528's row-menu test also expects #582's 'Add to Focus' entry now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-13 18:15:02 +02:00
1 parent a6ce52d3e7
commit 5d31e42db0
2 files changed
+20 -2

No files matched your search

+8 -1
View File
@@ -699,7 +699,14 @@
if (!state || state.state === 'dangling' || state.state === 'cycle') editError('no parent to follow');
if (state.state !== 'manual') return layout;
const targetKey = refKey(op.ref);
const clear = (ref) => (refKey(ref) === targetKey ? { kind: ref.kind, id: ref.id } : ref);
// Drop the placement only: a Focus pin stays (the whole layout is PUT,
// so a field left off here would be erased on save).
const clear = (ref) => {
if (refKey(ref) !== targetKey) return ref;
const copy = copyRef(ref);
delete copy.placement;
return copy;
};
for (const group of layout.groups) group.refs = group.refs.map(clear);
layout.ungrouped = layout.ungrouped.map(clear);
const childKeys = new Set();
+12 -1
View File
@@ -23,7 +23,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vite
const PUBLIC = join(process.cwd(), 'src/web/public');
const read = (name: string) => readFileSync(join(PUBLIC, name), 'utf8');
type Ref = { kind: 'session' | 'webview'; id: string; placement?: 'manual' };
type Ref = { kind: 'session' | 'webview'; id: string; placement?: 'manual'; focus?: true };
type Layout = {
version: number;
updatedAt: string;
@@ -133,6 +133,15 @@ describe('child sessions: follow and hand placement', () => {
expect(next.groups[1].refs).toEqual([]);
});
it('Follow parent again keeps a Focus pin: the whole layout is PUT, so a dropped field is erased', () => {
const pinned = lineage();
pinned.groups[1].refs[0] = { kind: 'session', id: 'moved', placement: 'manual', focus: true };
const next = h.applyOperation(pinned, { type: 'followParent', ref: s('moved'), parents });
const moved = next.groups[0].refs.find((ref: Ref) => ref.id === 'moved');
expect(moved.placement).toBeUndefined();
expect(moved.focus).toBe(true);
});
it('refuses to follow a parent that is gone or loops, and leaves a following child as it is', () => {
expect(() => h.applyOperation(lineage(), { type: 'followParent', ref: s('orphan'), parents })).toThrow();
expect(() =>
@@ -1078,8 +1087,10 @@ describe('child session placement in the row menu', () => {
installFetch();
const app = following(makeApp(lineageLayout()));
openMenu(app, 's1');
// 'Add to Focus' is #582's Focus section, offered on every row.
expect(menuLabels()).toEqual([
'Session options',
'Add to Focus',
'Move down',
'Move to "Later"',
'Move to Ungrouped',