diff --git a/ChatAI-Plasmoid.plasmoid b/ChatAI-Plasmoid.plasmoid
index 49f2d86..964e7c5 100644
Binary files a/ChatAI-Plasmoid.plasmoid and b/ChatAI-Plasmoid.plasmoid differ
diff --git a/README.md b/README.md
index a53f47c..f10546d 100644
--- a/README.md
+++ b/README.md
@@ -435,7 +435,10 @@ ChatAI-Plasmoid/
│ └── ui/ # QML: widget, WebView, provider registry, settings
│ ├── settings/ # Shared settings pages (dialog + in-widget panel)
│ └── assets/ # Provider and logo SVG icons
+├── docs/ # Technical notes (not shipped in the package)
├── locale/ # Translation sources (.pot / .po / .json)
+├── pkgbuild/ # Arch Linux / BigLinux package
+├── tests/ # Live regression tests for a running Plasma session
├── metadata.json # Plasma package metadata
├── LICENSE
└── README.md
@@ -461,6 +464,9 @@ for po in locale/*.po; do msgfmt --check --check-format -o /dev/null "$po"; done
# Run the widget in a standalone window
plasmawindowed ChatAI-Plasmoid
+
+# Popup size regression test (opens the popup on screen; see docs/TESTING.md)
+tests/popup-size-regression.sh 20 --fresh
```
Compiled translations (`contents/locale/**/*.mo`) ship in the package; regenerate
diff --git a/contents/config/main.xml b/contents/config/main.xml
index b5b0045..7ef018e 100644
--- a/contents/config/main.xml
+++ b/contents/config/main.xml
@@ -81,6 +81,8 @@
false
false
+
0
0
diff --git a/contents/ui/main.qml b/contents/ui/main.qml
index da7d335..73f477e 100644
--- a/contents/ui/main.qml
+++ b/contents/ui/main.qml
@@ -96,32 +96,20 @@ PlasmoidItem {
readonly property int defaultWidth: Math.max(Kirigami.Units.gridUnit * 28, minimumViewportWidth + Kirigami.Units.gridUnit * 2)
readonly property int defaultHeight: Kirigami.Units.gridUnit * 39
+ // Size of a popup the user never resized. Plasma's AppletPopup
+ // remembers the size the user picks (popupWidth/popupHeight in the
+ // applet config, saved on hide, restored on startup and kept within
+ // 95% of the screen), so these must not follow the measured size: the
+ // popup adds a 1px panel separator to them, and writing the measured
+ // size back (the old dialogWidth/dialogHeight) grew the popup by 1px
+ // every 500ms while it was open.
Layout.minimumWidth: Kirigami.Units.gridUnit * 18
Layout.minimumHeight: Kirigami.Units.gridUnit * 18
- Layout.preferredWidth: plasmoid.configuration.dialogWidth > 0 ? plasmoid.configuration.dialogWidth : defaultWidth
- Layout.preferredHeight: plasmoid.configuration.dialogHeight > 0 ? plasmoid.configuration.dialogHeight : defaultHeight
+ Layout.preferredWidth: defaultWidth
+ Layout.preferredHeight: defaultHeight
spacing: 0
- // Debounced persistence of the popup size.
- Timer {
- id: saveSizeTimer
- interval: 500
- repeat: false
- onTriggered: {
- const currentWidth = Math.round(mainLayout.width);
- const currentHeight = Math.round(mainLayout.height);
- if (currentWidth > 0 && currentHeight > 0
- && (currentWidth !== plasmoid.configuration.dialogWidth || currentHeight !== plasmoid.configuration.dialogHeight)) {
- plasmoid.configuration.dialogWidth = currentWidth;
- plasmoid.configuration.dialogHeight = currentHeight;
- }
- }
- }
-
- onWidthChanged: saveSizeTimer.restart()
- onHeightChanged: saveSizeTimer.restart()
-
Header {
id: headerRoot
diff --git a/docs/POPUP_SIZE_BUGFIX.md b/docs/POPUP_SIZE_BUGFIX.md
new file mode 100644
index 0000000..b244567
--- /dev/null
+++ b/docs/POPUP_SIZE_BUGFIX.md
@@ -0,0 +1,115 @@
+# Popup grows taller on its own
+
+## Symptom
+
+Opening ChatAI from its panel icon, the popup got a little taller every time,
+without the user resizing it, until it reached the 95% screen limit Plasma
+enforces. On a horizontal panel only the height grew; on a vertical panel the
+width grew instead. Both Wayland and X11 were affected.
+
+## Reproducing (before the fix)
+
+The growth happens in a Plasma session that started without a saved popup size
+for the widget: a newly added widget, the first session after installing, or
+after `popupWidth`/`popupHeight` were removed from its configuration.
+
+1. Add ChatAI to a horizontal panel (or remove `popupWidth`/`popupHeight` from
+ `[Containments][N][Applets][M][Configuration]` in
+ `~/.config/plasma-org.kde.plasma.desktop-appletsrc` and restart plasmashell).
+2. Open the popup and leave it open: it grows 1 px every 500 ms.
+3. Close and reopen it: the extra height is kept and keeps growing.
+
+`tests/popup-size-regression.sh --fresh` automates this (see
+[TESTING.md](TESTING.md)).
+
+## Root cause
+
+A feedback loop in `contents/ui/main.qml` between the size ChatAI asked for and
+the size it measured:
+
+```text
+Layout.preferredHeight <- plasmoid.configuration.dialogHeight
+ |
+ v Plasma sizes the popup from it: preferredHeight + 1 + frame padding
+mainLayout.height = preferredHeight + 1
+ |
+ v onHeightChanged -> saveSizeTimer (500 ms)
+plasmoid.configuration.dialogHeight = mainLayout.height -> back to the top, +1 px
+```
+
+The extra pixel comes from Plasma's popup wrapper for panel applets
+(`/usr/share/plasma/shells/org.kde.plasma.desktop/contents/applet/CompactApplet.qml`).
+It draws a 1 px separator line against the panel and adds it to the popup's size
+hint (`appletParent.extraHeight: 1` for top/bottom panels, `extraWidth: 1` for
+left/right panels). It only subtracts that pixel from the applet
+(`fullRepresentation.anchors.bottomMargin`) if the separator is already visible
+when the representation is attached, which was not the case here. So the applet
+gets one pixel more than it asked for.
+
+Logged in Plasma 6.7.4 (frame padding 15 px vertically):
+
+```text
+pref=1000x700 win=1016x716 content=1000x701
+SAVED content=702 pref=701 win=717
+SAVED content=703 pref=702 win=718 ... every 500 ms while open
+```
+
+Any difference between the requested and the received size would have caused
+the same drift; the loop turned a constant 1 px offset into unbounded growth.
+Whether that pixel shows up depends on how the separator is rounded at the
+current scale: at 150% (`QT_SCALE_FACTOR=1.5`) the old code did not grow, at
+100% and on a vertical panel it did.
+
+### Why it did not happen in every session
+
+Plasma 6's `AppletPopup` saves the popup size itself when the popup hides
+(`popupWidth`/`popupHeight`, without the frame padding) and restores it when
+plasmashell starts. A restored size makes Plasma ignore `Layout.preferredHeight`
+for the rest of that session, so the loop could not resize the popup. In a
+session without a saved size the loop ran freely, and Plasma then saved the
+inflated size, which is why it stayed large afterwards.
+
+## Fix
+
+`main.qml` no longer persists the size. `Layout.preferredWidth` and
+`Layout.preferredHeight` are constants (`defaultWidth`, `defaultHeight`): the
+size of a popup the user never resized. The `saveSizeTimer` and the
+`onWidthChanged`/`onHeightChanged` handlers are gone.
+
+Remembering a manual resize is left to Plasma, which already does it correctly:
+it saves the size on hide, restores it on startup, keeps it within 95% of the
+screen and is unaffected by theme margins. The popup is resized by Plasma's own
+resize handles, so a user resize is the only thing that changes that saved size.
+
+`dialogWidth`/`dialogHeight` stay in `contents/config/main.xml` because
+`Migration.js` uses `dialogWidth` to recognise older installations; they are
+no longer written or read for sizing.
+
+## Before and after
+
+Same machine and session state (no saved popup size), 30 openings of 1.2 s each:
+
+| Opening | 1 | 10 | 20 | 30 |
+|---|---|---|---|---|
+| Before | 701 | 725 | 755 | 788 (+88 px) |
+| After | 703 | 703 | 703 | 703 |
+
+A single 28 s opening grew the popup from 700 to 754 px before the fix. After
+it, 100 consecutive openings all saved 816×703. On X11 with a left panel the
+old code grew the width from 1175 to 1195 px in 5 openings; the fix kept
+817×702.
+
+## Known limitations
+
+- A popup that already grew keeps its saved size: the fix stops further growth
+ but cannot tell an inflated size from one the user chose. Resizing it once
+ fixes it for good.
+- The applet still receives `preferredHeight + 1` on a horizontal panel (the
+ separator compensation described above, in Plasma's own `CompactApplet.qml`).
+ It is now a constant pixel, not a cumulative one.
+- In a session without a saved popup size, a change of the default size (for
+ example a different font size, which changes `Kirigami.Units.gridUnit`) makes
+ Plasma resize the popup to the new default. This is Plasma's behaviour for
+ every applet; once a size is saved it no longer applies.
+- Validated on Wayland and X11, with bottom and left panels and at 100–200%
+ scale, on a single monitor; see [TESTING.md](TESTING.md).
diff --git a/docs/TECHNICAL_REVIEW.md b/docs/TECHNICAL_REVIEW.md
new file mode 100644
index 0000000..a8ccd09
--- /dev/null
+++ b/docs/TECHNICAL_REVIEW.md
@@ -0,0 +1,72 @@
+# Technical review: popup size fix
+
+## Changes
+
+| File | Change |
+|---|---|
+| `contents/ui/main.qml` | Constant `Layout.preferredWidth`/`Layout.preferredHeight`; removed `saveSizeTimer` and the size change handlers |
+| `contents/config/main.xml` | Comment marking `dialogWidth`/`dialogHeight` as legacy |
+| `ChatAI-Plasmoid.plasmoid` | Rebuilt: the committed package still contained the old `main.qml` |
+| `tests/popup-size-regression.sh` | New live regression test |
+| `docs/` | This review, [POPUP_SIZE_BUGFIX.md](POPUP_SIZE_BUGFIX.md) and [TESTING.md](TESTING.md) |
+| `README.md` | `docs/` and `tests/` in the Development section |
+
+## Decisions
+
+**Let Plasma own the popup size.** Plasma 6's `AppletPopup` already persists the
+popup size, saving it on hide and restoring it at startup, without frame margins
+and limited to 95% of the screen. ChatAI kept a second copy of the same
+information, and keeping both in sync is what created the loop. Removing ChatAI's
+copy fixes the cause instead of compensating for it, removes code, and makes
+ChatAI behave like the standard Plasma applets.
+
+Alternatives considered and rejected:
+
+- *Subtract the separator pixel before saving.* It depends on an internal detail
+ of Plasma's `CompactApplet.qml` (1 px, only on panel edges, only when not
+ compensated) and would break silently when that changes.
+- *Save only when the popup hides.* The measured size still includes the extra
+ pixel, so the growth would only slow down to 1 px per opening.
+- *Ignore size changes right after opening.* It needs a timing guess, and the
+ measured size is wrong for as long as the popup is open, not only at opening.
+- *Read the saved size once, without a live binding.* It stops the loop within a
+ session but still saves a size 1 px off, which then becomes the next
+ session's starting size.
+
+**Keep the legacy keys.** `dialogWidth`/`dialogHeight` stay in the KConfig schema
+because `Migration.js` treats `dialogWidth > 0` as a sign of an older
+installation. Existing values are harmless: nothing reads them for sizing any
+more. Users keep the size Plasma saved for them.
+
+**Defaults unchanged.** `defaultWidth` (at least 780 CSS px of viewport, so the
+AI sites keep their desktop layout) and `defaultHeight` (39 grid units) are the
+same values as before.
+
+## Performance
+
+- One `Timer` and two signal handlers fewer in `main.qml`.
+- In the affected state, the old loop resized the popup window, and with it the
+ Qt WebEngine view, every 500 ms for as long as the popup was open, writing the
+ configuration file each time. That no longer happens.
+- A CPU comparison over 20 s with the popup idle was dominated by the web page's
+ own activity and was inconclusive, so no figure is claimed.
+
+## Related audit findings
+
+- **Header animation.** `Behavior on Layout.preferredHeight` of the header only
+ redistributes space inside the layout. With an explicit preferred size on the
+ representation it cannot change the window, as the header tests in
+ [TESTING.md](TESTING.md) confirmed. It was kept.
+- **Content cannot resize the window.** Plasma reads the representation's
+ `Layout.preferredHeight`, which is explicit, so the WebView, the settings panel
+ and the auto-hide reveal strip do not affect the popup size.
+- **Committed package was stale.** `ChatAI-Plasmoid.plasmoid` had not been
+ rebuilt since the size persistence was last changed, so installing from it
+ would have kept the bug. It was rebuilt with the command from the README.
+- **Not ChatAI's code.** A malformed `[Containments2Appletsts]` group with
+ `dialogWidth`/`dialogHeight` was found in the test machine's
+ `plasma-org.kde.plasma.desktop-appletsrc`. Nothing in this repository or in
+ biglinux-welcome writes it; it comes from another tool writing to a wrong
+ group path and is harmless to ChatAI.
+
+No visual or interaction changes were made beyond the fix.
diff --git a/docs/TESTING.md b/docs/TESTING.md
new file mode 100644
index 0000000..b379552
--- /dev/null
+++ b/docs/TESTING.md
@@ -0,0 +1,108 @@
+# Testing the popup size
+
+How the fix in [POPUP_SIZE_BUGFIX.md](POPUP_SIZE_BUGFIX.md) was validated, and
+how to check it again.
+
+## Automated regression test
+
+`tests/popup-size-regression.sh` opens and closes the ChatAI popup in the running
+Plasma session and fails if the size Plasma saves on close ever changes.
+
+```bash
+tests/popup-size-regression.sh # 20 openings, current session
+tests/popup-size-regression.sh 100 # 100 openings
+tests/popup-size-regression.sh 20 --fresh # first restart plasmashell without a saved size
+```
+
+- Needs a Plasma 6 session (Wayland or X11) with ChatAI on a panel. The popup
+ opens on screen while it runs.
+- It toggles the popup through a temporary global shortcut (set with the
+ plasmashell scripting API, triggered over D-Bus through kglobalaccel) and
+ reads `popupWidth`/`popupHeight` after each close.
+- `--fresh` reproduces the state where the bug happened. It restarts
+ plasmashell twice (the panel disappears for a few seconds) and restores the
+ saved size, ChatAI's legacy `dialogWidth`/`dialogHeight` and the shortcut at
+ the end.
+- With the code before the fix it reports `FAIL`; with the fix, `PASS`. The
+ widget's configuration was byte-identical before and after each run.
+
+The test needs a real compositor and plasmashell, so it is not part of CI.
+
+## Static checks
+
+| Check | Result |
+|---|---|
+| `qmllint` (Qt 6.11) on every QML file | No new warnings: `main.qml` has the same 35 warnings as before (unqualified `plasmoid`/`i18n` access found throughout the project) |
+| `xmllint --noout contents/config/main.xml` | OK |
+| Package built from `metadata.json contents LICENSE` | Zip integrity OK, same file list as before, no dev files |
+| `kpackagetool6 -i` of that package into a temporary root | Installed |
+
+## Live tests
+
+Common setup: Plasma 6.7.4, KDE Frameworks 6.29, Qt 6.11.2, one 1920×1080
+screen, a 40 px panel, ChatAI as a panel widget. The code under test was
+installed as a temporary copy in `~/.local/share/plasma/plasmoids` (removed
+afterwards); "before" means the same tests against the code before the fix.
+
+### Wayland, bottom panel, 100% scale
+
+Window and content sizes were logged from QML through an instrumented copy of
+the widget.
+
+| Test | Result |
+|---|---|
+| 100 openings, no saved size | 100 × 816×703 |
+| 30 openings, same timing, before | 701 → 788 px (+88) |
+| 15 s opening | No size change while open (before: one 28 s opening grew 700 → 754 px) |
+| 3 plasmashell restarts × 5 openings, saved 1000×700 | 1000×700 throughout |
+| Resize the open popup by +60×+90 through the compositor (KWin script) | Saved as 1060×790, kept for 5 reopenings and after a restart; same in a session without a saved size (876×793) |
+| Header auto-hide on/off, header hidden on/off, popup open | Window 892×808 in all 864 logged samples; the header animation (31 → 0 px) stayed inside the layout |
+| Providers ChatGPT, Claude, Gemini, DeepSeek; pin on/off; popup open | Same 892×808 |
+| CPU of plasmashell + WebEngine, 20 s open and idle | Inconclusive: 15.8–28.5% across runs, dominated by the page's own activity |
+
+### X11
+
+Real mouse and keyboard input was generated with XTest (python-xlib), and the
+popup window was measured from the X server (`_KDE_NET_WM_WINDOW_TYPE_APPLET_POPUP`
+window of plasmashell).
+
+| Test | Before | After |
+|---|---|---|
+| Regression test, bottom panel, 100% | 457 → 481 px in 6 openings (`FAIL`) | 20 × 816×703 (`PASS`) |
+| Regression test, left (vertical) panel | Width 1175 → 1195 px in 5 openings (`FAIL`) | 10 × 817×702 (`PASS`) |
+| Regression test at `QT_SCALE_FACTOR` 1.25 / 1.5 / 2 | 1.5: stable 1073×454 | 816×703 / 816×669 / 816×498, all `PASS` |
+
+At 150% and 200% the height is Plasma's 95% screen limit in logical pixels
+(1080 / 1.5 × 0.95 and 1080 / 2 × 0.95, minus the frame), so oversized popups
+are clamped as expected. Before the fix the growth depended on how the
+separator pixel was rounded: at 150% it did not happen.
+
+With the fix, on the bottom panel at 100%:
+
+| Test | Result |
+|---|---|
+| Window vs saved size | Window 1090×469, saved 1074×454: the saved size excludes the frame padding (16×15 px) |
+| Drag the top edge 150 px up, then the left edge 100 px out, with the mouse | Window 1090×469 → 1090×619 → 1190×619, saved as 1174×604, kept for 3 reopenings and after a plasmashell restart |
+| Auto-hide header: pointer on the page 4.5 s, on the top strip, away again | Header hidden → shown → hidden (screenshots); window 1190×619 throughout |
+| Ctrl+F, then Escape | Find bar opens and closes; window unchanged |
+| F11, then Escape | Full screen window covers the screen and closes; popup unchanged |
+| Menu → About ChatAI, then Escape | In-widget settings open (About) and close; window unchanged |
+| Close button, then reopen | Popup closes, a WebEngine process ends (5 → 4), reopening recreates it (5) and loads the page; size unchanged |
+
+## Not verified
+
+- Multiple monitors: only one screen was available. Creating virtual monitors
+ with `xrandr --setmonitor` would have made Plasma add desktops and panels to
+ the tester's session, so it was not done.
+- Scale factors set in System Settings (they need a new login); the scale tests
+ used `QT_SCALE_FACTOR` on plasmashell, which scales it the same way.
+- A widget inside the System Tray: ChatAI does not declare
+ `X-Plasma-NotificationArea`, so it cannot be placed there.
+
+## Observations (unchanged behaviour)
+
+- Escape does not close the popup while the web page has focus: Qt WebEngine
+ hands it to the page, which sites use for their own dialogs. The shortcut
+ list in About documents Escape only for the find bar, settings and full
+ screen.
+- Tab moves focus inside the web page, not through the toolbar.
diff --git a/tests/popup-size-regression.sh b/tests/popup-size-regression.sh
new file mode 100755
index 0000000..1adc8e9
--- /dev/null
+++ b/tests/popup-size-regression.sh
@@ -0,0 +1,146 @@
+#!/bin/bash
+# Regression test for the popup size: opens and closes the ChatAI popup in the
+# running Plasma session and checks that the size Plasma saves on every close
+# never changes. See docs/POPUP_SIZE_BUGFIX.md.
+#
+# tests/popup-size-regression.sh [cycles] [--fresh]
+#
+# cycles open/close cycles (default 20)
+# --fresh restart plasmashell without Plasma's saved popup size first, the
+# state of a newly added widget (where the old code grew the popup);
+# the saved size is put back and plasmashell restarted at the end
+#
+# Needs a Plasma 6 session with ChatAI on a panel. The popup really opens on
+# screen while the test runs. A temporary global shortcut is used to toggle it
+# and is removed at the end, even on failure.
+
+set -euo pipefail
+
+cycles=20
+fresh=false
+for arg in "$@"; do
+ case $arg in
+ --fresh) fresh=true ;;
+ ''|*[!0-9]*) echo "usage: $0 [cycles] [--fresh]" >&2; exit 2 ;;
+ *) cycles=$arg ;;
+ esac
+done
+
+appletsrc=plasma-org.kde.plasma.desktop-appletsrc
+shortcut="Ctrl+Alt+Meta+F12"
+
+evaluate() {
+ qdbus6 org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript "$1"
+}
+
+# "containment applet" of the first ChatAI widget on a panel
+location=$(evaluate 'var r = ""; panels().forEach(function (p) { p.widgets().forEach(function (w) {
+ if (!r && w.type === "ChatAI-Plasmoid") r = p.id + " " + w.id; }); }); print(r);')
+if [ -z "$location" ]; then
+ echo "No ChatAI widget found on a panel" >&2
+ exit 2
+fi
+read -r containment applet <<< "$location"
+action="activate widget $applet"
+
+# Sets "entry" to the kconfig arguments of a widget setting: "popupWidth" (the
+# applet's Configuration group, where Plasma keeps the popup size),
+# "General/dialogWidth" (ChatAI's own settings) or "Shortcuts/global"
+widget_entry() {
+ entry=(--file "$appletsrc" --group Containments --group "$containment" --group Applets --group "$applet")
+ case $1 in
+ Shortcuts/*) entry+=(--group Shortcuts) ;;
+ */*) entry+=(--group Configuration --group "${1%%/*}") ;;
+ *) entry+=(--group Configuration) ;;
+ esac
+ entry+=(--key "${1##*/}")
+}
+
+config() {
+ widget_entry "$1"
+ kreadconfig6 "${entry[@]}"
+}
+
+write_config() {
+ widget_entry "$1"
+ kwriteconfig6 "${entry[@]}" "${@:2}"
+}
+
+set_shortcut() {
+ evaluate "panels().forEach(function (p) { p.widgets().forEach(function (w) {
+ if (w.id === $applet) w.globalShortcut = '$1'; }); });" > /dev/null
+}
+
+toggle() {
+ qdbus6 org.kde.kglobalaccel /component/plasmashell org.kde.kglobalaccel.Component.invokeShortcut "$action" > /dev/null
+}
+
+restart_plasmashell() {
+ # optional KEY=VALUE pairs to write while plasmashell is stopped ("" deletes)
+ systemctl --user stop plasma-plasmashell
+ local pair
+ for pair in "$@"; do
+ if [ -n "${pair#*=}" ]; then
+ write_config "${pair%%=*}" "${pair#*=}"
+ else
+ write_config "${pair%%=*}" --delete
+ fi
+ done
+ systemctl --user start plasma-plasmashell
+ local i
+ for i in $(seq 1 30); do
+ evaluate 'print(1)' > /dev/null 2>&1 && break
+ sleep 1
+ done
+ sleep 3
+}
+
+saved_width=$(config popupWidth)
+saved_height=$(config popupHeight)
+# Versions before the fix also write their own size here while the test runs
+saved_dialog_width=$(config General/dialogWidth)
+saved_dialog_height=$(config General/dialogHeight)
+saved_shortcut=$(config Shortcuts/global)
+
+cleanup() {
+ set_shortcut "$saved_shortcut" || true
+ if $fresh; then
+ # plasmashell writes an empty "global=" back while running
+ restart_plasmashell "popupWidth=$saved_width" "popupHeight=$saved_height" \
+ "General/dialogWidth=$saved_dialog_width" "General/dialogHeight=$saved_dialog_height" \
+ "Shortcuts/global=$saved_shortcut" || true
+ fi
+ if [ -z "$saved_shortcut" ]; then
+ # kglobalaccel keeps the action in memory; editing its file is not enough
+ qdbus6 org.kde.kglobalaccel /kglobalaccel org.kde.KGlobalAccel.unregister plasmashell "$action" > /dev/null || true
+ fi
+}
+trap cleanup EXIT
+
+if $fresh; then
+ restart_plasmashell popupWidth= popupHeight=
+fi
+
+set_shortcut "$shortcut"
+for i in $(seq 1 30); do
+ qdbus6 org.kde.kglobalaccel /component/plasmashell org.kde.kglobalaccel.Component.shortcutNames \
+ | grep -q "$action" && break
+ sleep 0.5
+done
+
+declare -A seen=()
+first=""
+for i in $(seq 1 "$cycles"); do
+ toggle; sleep 1.5
+ toggle; sleep 1
+ size="$(config popupWidth)x$(config popupHeight)"
+ printf '%3d %s\n' "$i" "$size"
+ first=${first:-$size}
+ seen[$size]=1
+done
+
+if [ "${#seen[@]}" -ne 1 ]; then
+ echo "FAIL: the popup size changed across $cycles openings (first $first, last $size)" >&2
+ exit 1
+fi
+echo "PASS: $cycles openings, size $first every time"