Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified ChatAI-Plasmoid.plasmoid
Binary file not shown.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions contents/config/main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
<!-- Legacy alias of pin (ChatAI < 1.0). Migrated. -->
<entry name="keepOpen" type="Bool"><default>false</default></entry>
<entry name="loadOnStartup" type="Bool"><default>false</default></entry>
<!-- Legacy popup size, no longer written (Plasma keeps popupWidth/popupHeight);
read by Migration.js to recognise older installations -->
<entry name="dialogWidth" type="Int"><default>0</default></entry>
<entry name="dialogHeight" type="Int"><default>0</default></entry>

Expand Down
30 changes: 9 additions & 21 deletions contents/ui/main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
115 changes: 115 additions & 0 deletions docs/POPUP_SIZE_BUGFIX.md
Original file line number Diff line number Diff line change
@@ -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).
72 changes: 72 additions & 0 deletions docs/TECHNICAL_REVIEW.md
Original file line number Diff line number Diff line change
@@ -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.
108 changes: 108 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading