Skip to content

Commit 6914b5c

Browse files
committed
fix(app): the desktop panel is centred on purpose — say so, and test it
Fourth review thread. The doc comment promised "an anchored popover on desktop. Same split as ContextUsageButton (SPEC-37)" and D11 specified a MenuAnchor; the code ships showDialog + Alignment.center. So this was not a stale comment, it was an unrecorded spec deviation. Kept the centring, and the reason is a real finding rather than a shrug: the mechanism cannot transfer, because the DOOR TOPOLOGY differs. ContextUsageButton is a persistent control in the composer, so a MenuAnchor has something to stay anchored to for as long as the popover is open. Both of this panel's desktop doors are transient MENU ITEMS -- the pane-header kebab and the mobile glass menu -- so by the time an item is chosen its menu has been dismissed. Anchoring to where a vanished menu item used to be is arbitrary placement dressed up as precision. What SPEC-37 actually contributes is kept: the window-clamped width and the SingleChildScrollView, so a panel opened from a narrow split pane cannot hang off-screen. The real defect underneath: the desktop host had NO test. Every case in session_identity_widget_test.dart passed `desktop: false`, which is exactly how the code and its own doc comment drifted apart and stayed that way through implementation. Two tests added: * desktop opens a centred, window-clamped panel -- not a sheet * a desktop panel in a narrow window is clamped to the window (300pt window, 340pt panel, so the clamp is load-bearing) Both mutation-proven: Alignment.center -> topLeft fails the first, and dropping the math.min window clamp fails the second. D11 amended in the spec and logged as deviation 8, so the next reader does not find a third version of the truth.
1 parent 1a0bb8a commit 6914b5c

4 files changed

Lines changed: 92 additions & 3 deletions

File tree

app/lib/ui/session/session_identity.dart

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,8 +431,25 @@ class _CopyAllRow extends ConsumerWidget {
431431
}
432432
}
433433

434-
/// Opens the identity panel: a modal bottom sheet on mobile, an anchored
435-
/// popover on desktop. Same split as `ContextUsageButton` (SPEC-37).
434+
/// Opens the identity panel: a modal bottom sheet on mobile, a **centred,
435+
/// window-clamped** panel on desktop.
436+
///
437+
/// Centred on purpose, and NOT the `MenuAnchor` popover D11 first specified.
438+
/// D11 said "verbatim the `ContextUsageButton` split", but the mechanism cannot
439+
/// transfer, because the door topology is different: `ContextUsageButton` is a
440+
/// persistent control in the composer, so a `MenuAnchor` has something to anchor
441+
/// to for as long as the popover is open. Both of this panel's desktop doors are
442+
/// transient MENU ITEMS (the pane-header kebab, the mobile glass menu) — by the
443+
/// time an item is chosen its menu has been dismissed, so there is nothing left
444+
/// on screen to anchor to, and anchoring to where a vanished item used to be is
445+
/// arbitrary placement dressed up as precision.
446+
///
447+
/// What was kept from SPEC-37 is the part that matters and is testable: the
448+
/// window-clamped width and the `SingleChildScrollView`, so a panel opened from a
449+
/// narrow split pane cannot hang off-screen. Both properties are pinned by tests
450+
/// in `test/session_identity_widget_test.dart` ("desktop opens a centred,
451+
/// window-clamped panel"), which exist because the desktop host previously had no
452+
/// test at all — which is exactly how the code and this comment drifted apart.
436453
///
437454
/// Pass EXACTLY ONE of [sessionId] or [identity]:
438455
///

app/test/session_identity_widget_test.dart

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,77 @@ void main() {
313313
expect(find.text(kId), findsOneWidget);
314314
});
315315

316+
testWidgets('desktop opens a centred, window-clamped panel — not a sheet', (
317+
tester,
318+
) async {
319+
// The desktop host had NO test at all: every case here passed
320+
// `desktop: false`, which is how the panel came to be documented as an
321+
// "anchored MenuAnchor popover" (D11) while shipping a centred dialog. It
322+
// is centred on purpose — both desktop doors are transient MENU ITEMS, so
323+
// by the time one is chosen the menu is gone and there is nothing left on
324+
// screen to anchor to (unlike SPEC-37's ContextUsageButton, a persistent
325+
// control in the composer). This test pins the presentation that actually
326+
// ships, so the doc and the code cannot drift apart again.
327+
tester.view.physicalSize = const Size(1400, 900);
328+
tester.view.devicePixelRatio = 1.0;
329+
addTearDown(tester.view.reset);
330+
await tester.pumpWidget(
331+
_host(
332+
Builder(
333+
builder: (context) => ElevatedButton(
334+
onPressed: () => showSessionIdentity(
335+
context: context,
336+
identity: identity(),
337+
desktop: true,
338+
),
339+
child: const Text('open'),
340+
),
341+
),
342+
),
343+
);
344+
await tester.tap(find.text('open'));
345+
await tester.pumpAndSettle();
346+
expect(find.byType(BottomSheet), findsNothing);
347+
expect(find.byType(SessionIdentityDetails), findsOneWidget);
348+
// Centred: the panel's centre sits on the window's centre, both axes.
349+
final panel = tester.getRect(find.byType(SessionIdentityDetails));
350+
expect(panel.center.dx, moreOrLessEquals(700, epsilon: 1));
351+
expect(panel.center.dy, moreOrLessEquals(450, epsilon: 1));
352+
// Window-clamped, never wider than the fixed panel width (SPEC-37).
353+
expect(panel.width, lessThanOrEqualTo(kIdentityPanelWidth));
354+
});
355+
356+
testWidgets('a desktop panel in a narrow window is clamped to the window', (
357+
tester,
358+
) async {
359+
// The SPEC-37 lesson for the desktop host: a fixed-width panel opened from
360+
// a narrow split pane hung off-screen. 300pt is narrower than the panel's
361+
// 340pt, so the clamp is what keeps both margins.
362+
tester.view.physicalSize = const Size(300, 700);
363+
tester.view.devicePixelRatio = 1.0;
364+
addTearDown(tester.view.reset);
365+
await tester.pumpWidget(
366+
_host(
367+
Builder(
368+
builder: (context) => ElevatedButton(
369+
onPressed: () => showSessionIdentity(
370+
context: context,
371+
identity: identity(),
372+
desktop: true,
373+
),
374+
child: const Text('open'),
375+
),
376+
),
377+
),
378+
);
379+
await tester.tap(find.text('open'));
380+
await tester.pumpAndSettle();
381+
final panel = tester.getRect(find.byType(SessionIdentityDetails));
382+
expect(panel.left, greaterThanOrEqualTo(0));
383+
expect(panel.right, lessThanOrEqualTo(300));
384+
expect(panel.width, lessThanOrEqualTo(300 - 2 * 12));
385+
});
386+
316387
testWidgets(
317388
'at 320x360 nothing paints off-screen and Copy all is still reachable',
318389
(tester) async {

docs/specs/2026-08-11-SPEC-52-PLAN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,4 @@ Per `makit-transcript-row-qa-harness`:
243243
| 5 | D12 (identity section inside the usage panel) **cut** from P1 to P2. | The ring is absent in four states including the likeliest moment of need, so that door is missing exactly when wanted; it also added an import edge and mixed two row styles in one panel. | Spec D12, rev 2. |
244244
| 6 | Three panel doors → **two**; the desktop tab menu keeps only *Copy session id*. | A tab-menu *Session details…* duplicates the pane-header kebab one pixel away on the same platform. | Spec D13, rev 2. |
245245
| 7 | Added D18 (a11y), D19 (watch not snapshot), D20 (i18n), D21 (path disclosure). | Review found a11y absent where SPEC-47 had locked it; a panel opened before the id is assigned would have shown stale rows. | Spec rev 2. |
246+
| 8 | D11's desktop host is a **centred, window-clamped panel**, not the specified `MenuAnchor` popover. | The mechanism cannot transfer: `ContextUsageButton` is a persistent composer control, so a `MenuAnchor` stays anchored to it, whereas both of this panel's desktop doors are transient menu items — by the time one is chosen the menu is dismissed and there is nothing left to anchor to. Found by review of the shipped code, not by a test, because the desktop host had **no test at all** (every case passed `desktop: false`) — which is how the code and its own doc comment drifted apart. | Spec D11 amended; two desktop tests added, both mutation-proven (alignment → topLeft, and dropping the clamp). |

docs/specs/2026-08-11-SPEC-52-session-identity.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ them. **The absence rule is not weakened** (D12) — the panel is reachable with
107107
| **D8** | The panel is **read-only**. No rename, no regenerate, no delete, no resume button. | Lifecycle already lives in the same menus (Close / Quit agent) and must not sit one mis-tap from the copy row. |
108108
| **D9** | A row whose value is unmeasured is **omitted**, never rendered blank or as a placeholder. `Copy all` then copies fewer lines. | Same rule SPEC-37 settled on for the ring. A fabricated path is worse than no path: it will be pasted into a prompt and the next agent will report it missing. |
109109
| **D10** | Per-agent vocabulary is a **lookup table** keyed by `SessionDTO.agent` (not a `switch`): pi → label `pi session`, resume `pi --session <id>`; codex → label `Thread`, resume `codex resume <id>`; anything else → label `Agent session`, **no** resume row. | Codex's own word for it is a thread (`thread/start``thread.id`). An unknown ACP agent gets no resume line because we do not know its CLI — inventing one is D9's failure mode in command form. A table rather than a `switch` because `docs/ENGINEERING.md`'s OCP rule is explicit: adding an adapter must not mean editing a growing `switch`. The safe default *is* the open/closed escape hatch — a third agent works unedited, just without a resume line. |
110-
| **D11** | One host-agnostic body (`SessionIdentityDetails`) presented as a modal bottom sheet on mobile and a `MenuAnchor` popover on desktop. | Verbatim the `ContextUsageButton` / `ContextUsageDetails` split (`context_usage.dart:199-300`), including the window-clamped width and the `SingleChildScrollView`. |
110+
| **D11** | One host-agnostic body (`SessionIdentityDetails`) presented as a modal bottom sheet on mobile and a **centred, window-clamped panel** on desktop. | Originally specified as a `MenuAnchor` popover, "verbatim the `ContextUsageButton` / `ContextUsageDetails` split (`context_usage.dart:199-300`)". **Amended on review of the implementation (deviation 8):** the mechanism does not transfer, because the door topology differs. `ContextUsageButton` is a *persistent* composer control, so a `MenuAnchor` has something to stay anchored to; both of this panel's desktop doors are *transient menu items*, and by the time one is chosen its menu is gone. What the split actually contributes — the window-clamped width and the `SingleChildScrollView`, so a panel opened from a narrow split pane cannot hang off-screen — is kept and is now pinned by tests. |
111111
| **D12** | **CUT from P1 → P2.** No `SessionIdentitySection` inside `ContextUsageDetails`. The ring's absence rule (`context_usage.dart:218`) is untouched, as before. | Cut on review, and the argument is this spec's own: the ring is absent in the four states above, *including the likeliest moment of need*, so a door hung off it is missing exactly when it is wanted — near-zero marginal value on top of the two menu doors and `/session`. It is not free either: it adds a `session_identity → context_usage` import edge, and it would put stacked mono rows beside `_Row`'s label/value rows in one panel. Deferring also deletes a whole task whose test was checking the wrong invariant (see §Review findings). |
112112
| **D13** | **Two** panel doors, in menus that already exist and are always present: mobile `_glassMenu` (`session_screen.dart:505`) and the desktop pane-header kebab (`pane_header.dart:154`). The desktop **tab** menu gets **Copy session id** only. | Cut from three on review: a tab-menu *Session details…* is redundant with a pane-header kebab one pixel away on the same platform. The tab menu keeps **Copy session id** because that is a different job (right-click → one click → done), not a second way to open the same sheet. Zero permanent chrome either way; SPEC-40's 375 pt crowding is untouched. |
113113
| **D14** | The clipboard payload is produced by one **pure** function, `sessionIdentityText()`, shared verbatim by the panel, `/session` and both menus. Format: one `label: value` per line, labels padded to a common width, absolute paths, omitted rows absent. | Pure ⇒ unit-tested directly, the same seam `formatTokens` / `headroomLabel` use. One function ⇒ the copy contract cannot diverge between four call sites. Plain lines survive being pasted into a prompt, a commit message, an issue or a terminal comment; markdown or JSON would need escaping. |

0 commit comments

Comments
 (0)