From d04d0d540469245f3406fe97ceb5a042a354db31 Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 18:03:54 +1000 Subject: [PATCH 01/13] feat(notifications): add read controls and paging Improve the per-notification read affordance and page unread notifications in five-item windows.\n\nValidation:\n- tests/panel-source-test.sh\n- tests/service-source-test.sh\n- tests/helper-test.sh\n- omarchy plugin validate .\n\nAssisted-by: Codex/GPT-5 --- Panel.qml | 90 ++++++++++++++++++++++++++++++++------ tests/panel-source-test.sh | 12 ++++- 2 files changed, 88 insertions(+), 14 deletions(-) diff --git a/Panel.qml b/Panel.qml index f9edc9c..ebbe2f6 100644 --- a/Panel.qml +++ b/Panel.qml @@ -21,7 +21,7 @@ Panel { property string sortMode: "updated" property bool cursorActive: false property int cursorIndex: 0 - property bool notificationsExpanded: false + property int notificationsPage: 0 property bool reviewsExpanded: false property bool myPullsExpanded: false property bool issuesExpanded: false @@ -70,12 +70,23 @@ Panel { return rows.slice(0, expanded ? activityExpandedCount : activityPreviewCount) } + function notificationPageCount() { + return Math.max(1, Math.ceil(github.notifications.length / activityPreviewCount)) + } + + function notificationRows() { + var page = Math.max(0, Math.min(notificationsPage, notificationPageCount() - 1)) + if (page !== notificationsPage) notificationsPage = page + var start = page * activityPreviewCount + return github.notifications.slice(start, start + activityPreviewCount) + } + function buildCursorTargets() { var targets = [] function add(kind, rows) { for (var i = 0; i < rows.length; i++) targets.push({ key: kind + ":" + String(rows[i].id || rows[i].url || i), kind: kind, row: rows[i] }) } - add("notification", sectionRows(github.notifications, notificationsExpanded)) + add("notification", notificationRows()) add("review", sectionRows(github.reviewRequests, reviewsExpanded)) add("mypull", sectionRows(github.myPullRequests, myPullsExpanded)) add("issue", sectionRows(github.assignedIssues, issuesExpanded)) @@ -456,10 +467,12 @@ Panel { title: "UNREAD NOTIFICATIONS" count: github.notifications.length emptyText: github.state === "ready" ? "You're all caught up." : "No notifications loaded." - model: root.sectionRows(github.notifications, root.notificationsExpanded) - expanded: root.notificationsExpanded + model: root.notificationRows() + page: root.notificationsPage + pageCount: root.notificationPageCount() openUrl: "https://github.com/notifications" - onToggleExpanded: root.notificationsExpanded = !root.notificationsExpanded + onPreviousPage: root.notificationsPage = Math.max(0, root.notificationsPage - 1) + onNextPage: root.notificationsPage = Math.min(root.notificationPageCount() - 1, root.notificationsPage + 1) delegateComponent: notificationDelegate actionText: "Mark all read" actionBusyText: "Marking…" @@ -968,6 +981,8 @@ Panel { property Component delegateComponent: null property bool expanded: false property string openUrl: "" + property int page: 0 + property int pageCount: 1 // Optional destructive action. It arms on the first click and only runs on // the second, so a stray click cannot clear the section. property string actionText: "" @@ -980,6 +995,8 @@ Panel { property var actionPrepare: null property string preparedAction: "" signal toggleExpanded() + signal previousPage() + signal nextPage() signal actionTriggered(string prepared) function disarmAction() { @@ -1029,9 +1046,10 @@ Panel { // Expanding is only offered once the section is truncated; below that // threshold the remaining controls render unbordered on their own line. readonly property bool expandable: section.count > root.activityPreviewCount + readonly property bool paginated: section.pageCount > 1 readonly property bool showOpen: section.count > 0 && section.openUrl !== "" readonly property bool showAction: section.count > 0 && section.actionEnabled && section.actionText !== "" - visible: expandable || showOpen || showAction + visible: expandable || paginated || showOpen || showAction anchors.horizontalCenter: parent.horizontalCenter spacing: Style.space(12) Button { @@ -1044,6 +1062,38 @@ Panel { verticalPadding: Style.spacing.controlPaddingY onClicked: section.toggleExpanded() } + Button { + visible: sectionFooter.paginated + text: "󰅁" + tooltipText: "Previous notifications" + bordered: true + foreground: root.foreground + fontFamily: root.fontFamily + fontSize: Style.font.caption + verticalPadding: Style.spacing.controlPaddingY + enabled: section.page > 0 + onClicked: section.previousPage() + } + Text { + visible: sectionFooter.paginated + text: (section.page + 1) + " / " + section.pageCount + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + verticalAlignment: Text.AlignVCenter + } + Button { + visible: sectionFooter.paginated + text: "󰅂" + tooltipText: "Next notifications" + bordered: true + foreground: root.foreground + fontFamily: root.fontFamily + fontSize: Style.font.caption + verticalPadding: Style.spacing.controlPaddingY + enabled: section.page + 1 < section.pageCount + onClicked: section.nextPage() + } Button { id: actionButton // The confirm and busy labels are shorter than the idle one. Letting the @@ -1157,15 +1207,29 @@ Panel { elide: Text.ElideRight } } - PanelActionButton { + BorderSurface { visible: linkRow.showReadAction - enabled: github.markingNotificationId !== linkRow.notificationId - iconText: github.markingNotificationId === linkRow.notificationId ? "󰑐" : "󰄬" - tooltipText: "Mark this notification read (M)" - foreground: root.foreground - fontFamily: root.fontFamily Layout.alignment: Qt.AlignVCenter - onClicked: github.markNotificationRead(linkRow.notificationId) + Layout.fillHeight: true + implicitWidth: readAction.size + implicitHeight: readAction.size + radius: Style.cornerRadius + color: Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.14) + borderSpec: Border.flat(Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.42), 1) + + PanelActionButton { + id: readAction + anchors.centerIn: parent + enabled: github.markingNotificationId !== linkRow.notificationId + iconText: github.markingNotificationId === linkRow.notificationId ? "󰑐" : "󰄬" + tooltipText: "Mark this notification read (M)" + foreground: root.foreground + hoverColor: Color.accent + fontFamily: root.fontFamily + size: Math.max(Style.space(32), fontSize + Style.spacing.sm * 2) + bordered: true + onClicked: github.markNotificationRead(linkRow.notificationId) + } } Text { text: "󰅂"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.body } } diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index ac65aa4..c2ce0c3 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -57,8 +57,18 @@ assert_contains 'visible: root.settingsOpen' \ "the settings page is always visible" assert_contains $'pageFlip.stop()\n settingsOpen = false' \ "closing the panel leaves it on the settings page" -assert_contains $'PanelActionButton {\n visible: linkRow.showReadAction\n enabled: github.markingNotificationId !== linkRow.notificationId' \ +assert_contains $'BorderSurface {\n visible: linkRow.showReadAction\n Layout.alignment: Qt.AlignVCenter\n Layout.fillHeight: true' \ "notification row marking is disabled during refresh" +assert_contains $'color: Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.14)\n borderSpec: Border.flat(Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.42), 1)' \ + "notification read action is not visually distinct" +assert_contains $'function notificationRows() {\n var page = Math.max(0, Math.min(notificationsPage, notificationPageCount() - 1))' \ + "notifications are not paged in five-item windows" +assert_contains $'onPreviousPage: root.notificationsPage = Math.max(0, root.notificationsPage - 1)\n onNextPage: root.notificationsPage = Math.min(root.notificationPageCount() - 1, root.notificationsPage + 1)' \ + "notification page controls do not clamp their range" +assert_contains $'text: "󰅁"\n tooltipText: "Previous notifications"' \ + "previous notification page control is missing" +assert_contains $'text: "󰅂"\n tooltipText: "Next notifications"' \ + "next notification page control is missing" assert_contains $'function applyPanelWheel(event) {\n if (!panelFlick || (sortPicker && sortPicker.popup.visible)) return false' \ "the panel still uses Flickable's default wheel distance" From 47d25beafed3873335d86cd67ad218ee4f5cf836 Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 19:31:11 +1000 Subject: [PATCH 02/13] fix(notifications): soften read action styling Keep the larger notification read target while reducing its idle contrast against the panel.\n\nValidation:\n- tests/panel-source-test.sh\n- tests/service-source-test.sh\n- tests/helper-test.sh\n- omarchy plugin validate .\n\nAssisted-by: Codex/GPT-5 --- Panel.qml | 4 ++-- tests/panel-source-test.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Panel.qml b/Panel.qml index ebbe2f6..4e47555 100644 --- a/Panel.qml +++ b/Panel.qml @@ -1214,8 +1214,8 @@ Panel { implicitWidth: readAction.size implicitHeight: readAction.size radius: Style.cornerRadius - color: Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.14) - borderSpec: Border.flat(Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.42), 1) + color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.06) + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.16), 1) PanelActionButton { id: readAction diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index c2ce0c3..9703024 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -59,8 +59,8 @@ assert_contains $'pageFlip.stop()\n settingsOpen = false' \ "closing the panel leaves it on the settings page" assert_contains $'BorderSurface {\n visible: linkRow.showReadAction\n Layout.alignment: Qt.AlignVCenter\n Layout.fillHeight: true' \ "notification row marking is disabled during refresh" -assert_contains $'color: Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.14)\n borderSpec: Border.flat(Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.42), 1)' \ - "notification read action is not visually distinct" +assert_contains $'color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.06)\n borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.16), 1)' \ + "notification read action is not subtly distinct" assert_contains $'function notificationRows() {\n var page = Math.max(0, Math.min(notificationsPage, notificationPageCount() - 1))' \ "notifications are not paged in five-item windows" assert_contains $'onPreviousPage: root.notificationsPage = Math.max(0, root.notificationsPage - 1)\n onNextPage: root.notificationsPage = Math.min(root.notificationPageCount() - 1, root.notificationsPage + 1)' \ From 36ba21b45f24857762ab833fdcca33d92f59906e Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 19:31:46 +1000 Subject: [PATCH 03/13] fix(notifications): align page counter Center the notification page counter against its arrow controls.\n\nValidation:\n- tests/panel-source-test.sh\n- tests/service-source-test.sh\n- tests/helper-test.sh\n- omarchy plugin validate .\n\nAssisted-by: Codex/GPT-5 --- Panel.qml | 2 ++ tests/panel-source-test.sh | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Panel.qml b/Panel.qml index 4e47555..79e9c76 100644 --- a/Panel.qml +++ b/Panel.qml @@ -1063,6 +1063,7 @@ Panel { onClicked: section.toggleExpanded() } Button { + id: previousPageButton visible: sectionFooter.paginated text: "󰅁" tooltipText: "Previous notifications" @@ -1077,6 +1078,7 @@ Panel { Text { visible: sectionFooter.paginated text: (section.page + 1) + " / " + section.pageCount + height: previousPageButton.height color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.caption diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index 9703024..3b8e873 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -69,6 +69,8 @@ assert_contains $'text: "󰅁"\n tooltipText: "Previous notifications"' \ "previous notification page control is missing" assert_contains $'text: "󰅂"\n tooltipText: "Next notifications"' \ "next notification page control is missing" +assert_contains $'text: (section.page + 1) + " / " + section.pageCount\n height: previousPageButton.height\n color: root.dim' \ + "notification page number is not vertically centered with its controls" assert_contains $'function applyPanelWheel(event) {\n if (!panelFlick || (sortPicker && sortPicker.popup.visible)) return false' \ "the panel still uses Flickable's default wheel distance" From 6186c66ee9f0d34c6d2aebe8fd7038c78bc9b17c Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 19:32:12 +1000 Subject: [PATCH 04/13] fix(notifications): remove inactive expansion control Paginated notifications should expose page controls and bulk read, not the generic show-all button.\n\nValidation:\n- tests/panel-source-test.sh\n- tests/service-source-test.sh\n- tests/helper-test.sh\n- omarchy plugin validate .\n\nAssisted-by: Codex/GPT-5 --- Panel.qml | 4 +++- tests/panel-source-test.sh | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Panel.qml b/Panel.qml index 79e9c76..227f10c 100644 --- a/Panel.qml +++ b/Panel.qml @@ -468,6 +468,7 @@ Panel { count: github.notifications.length emptyText: github.state === "ready" ? "You're all caught up." : "No notifications loaded." model: root.notificationRows() + showExpansionControl: false page: root.notificationsPage pageCount: root.notificationPageCount() openUrl: "https://github.com/notifications" @@ -980,6 +981,7 @@ Panel { property var model: [] property Component delegateComponent: null property bool expanded: false + property bool showExpansionControl: true property string openUrl: "" property int page: 0 property int pageCount: 1 @@ -1045,7 +1047,7 @@ Panel { id: sectionFooter // Expanding is only offered once the section is truncated; below that // threshold the remaining controls render unbordered on their own line. - readonly property bool expandable: section.count > root.activityPreviewCount + readonly property bool expandable: section.showExpansionControl && section.count > root.activityPreviewCount readonly property bool paginated: section.pageCount > 1 readonly property bool showOpen: section.count > 0 && section.openUrl !== "" readonly property bool showAction: section.count > 0 && section.actionEnabled && section.actionText !== "" diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index 3b8e873..503a3cf 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -65,6 +65,8 @@ assert_contains $'function notificationRows() {\n var page = Math.max(0, Math "notifications are not paged in five-item windows" assert_contains $'onPreviousPage: root.notificationsPage = Math.max(0, root.notificationsPage - 1)\n onNextPage: root.notificationsPage = Math.min(root.notificationPageCount() - 1, root.notificationsPage + 1)' \ "notification page controls do not clamp their range" +assert_contains $'model: root.notificationRows()\n showExpansionControl: false\n page: root.notificationsPage' \ + "notification pagination still shows an inactive expansion control" assert_contains $'text: "󰅁"\n tooltipText: "Previous notifications"' \ "previous notification page control is missing" assert_contains $'text: "󰅂"\n tooltipText: "Next notifications"' \ From fb6ffa6dea2f0c3c41ec058bb4df6ab403faa8e7 Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 19:35:55 +1000 Subject: [PATCH 05/13] fix(notifications): clarify read controls Keep bulk read visible while notification data refreshes and remove the misleading open-link indicator from read-toggle rows.\n\nValidation:\n- tests/panel-source-test.sh\n- tests/service-source-test.sh\n- tests/helper-test.sh\n- omarchy plugin validate .\n\nAssisted-by: Codex/GPT-5 --- Panel.qml | 74 +++++++++++++++++++++----------------- tests/panel-source-test.sh | 4 +++ 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/Panel.qml b/Panel.qml index 227f10c..2503ed5 100644 --- a/Panel.qml +++ b/Panel.qml @@ -877,6 +877,7 @@ Panel { detail: modelData.repository + " · " + modelData.reason + " · " + root.relativeTime(modelData.updatedAt) url: modelData.url showReadAction: true + showTrailingIndicator: false notificationId: String(modelData.id || "") } } @@ -1050,7 +1051,7 @@ Panel { readonly property bool expandable: section.showExpansionControl && section.count > root.activityPreviewCount readonly property bool paginated: section.pageCount > 1 readonly property bool showOpen: section.count > 0 && section.openUrl !== "" - readonly property bool showAction: section.count > 0 && section.actionEnabled && section.actionText !== "" + readonly property bool showAction: section.count > 0 && section.actionText !== "" visible: expandable || paginated || showOpen || showAction anchors.horizontalCenter: parent.horizontalCenter spacing: Style.space(12) @@ -1064,6 +1065,37 @@ Panel { verticalPadding: Style.spacing.controlPaddingY onClicked: section.toggleExpanded() } + Button { + id: actionButton + // The confirm and busy labels are shorter than the idle one. Letting the + // button shrink would slide its neighbours under a pointer that is about + // to click again, so the widest label seen so far sets the width. + property real reservedWidth: 0 + onImplicitWidthChanged: reservedWidth = Math.max(reservedWidth, implicitWidth) + width: Math.max(reservedWidth, implicitWidth) + visible: sectionFooter.showAction + enabled: section.actionEnabled && !section.actionBusy + text: section.actionBusy ? section.actionBusyText : (section.actionArmed ? section.actionConfirmText : section.actionText) + bordered: sectionFooter.expandable + foreground: section.actionArmed ? root.urgent : root.foreground + fontFamily: root.fontFamily + fontSize: Style.font.caption + verticalPadding: Style.spacing.controlPaddingY + onClicked: { + if (section.actionBusy) return + if (!section.actionArmed) { + var prepared = section.actionPrepare ? String(section.actionPrepare() || "") : "confirmed" + if (prepared === "") return + section.preparedAction = prepared + section.actionArmed = true + actionArmTimer.restart() + return + } + var confirmed = section.preparedAction + section.disarmAction() + section.actionTriggered(confirmed) + } + } Button { id: previousPageButton visible: sectionFooter.paginated @@ -1098,37 +1130,6 @@ Panel { enabled: section.page + 1 < section.pageCount onClicked: section.nextPage() } - Button { - id: actionButton - // The confirm and busy labels are shorter than the idle one. Letting the - // button shrink would slide its neighbours under a pointer that is about - // to click again, so the widest label seen so far sets the width. - property real reservedWidth: 0 - onImplicitWidthChanged: reservedWidth = Math.max(reservedWidth, implicitWidth) - width: Math.max(reservedWidth, implicitWidth) - visible: sectionFooter.showAction - enabled: !section.actionBusy - text: section.actionBusy ? section.actionBusyText : (section.actionArmed ? section.actionConfirmText : section.actionText) - bordered: sectionFooter.expandable - foreground: section.actionArmed ? root.urgent : root.foreground - fontFamily: root.fontFamily - fontSize: Style.font.caption - verticalPadding: Style.spacing.controlPaddingY - onClicked: { - if (section.actionBusy) return - if (!section.actionArmed) { - var prepared = section.actionPrepare ? String(section.actionPrepare() || "") : "confirmed" - if (prepared === "") return - section.preparedAction = prepared - section.actionArmed = true - actionArmTimer.restart() - return - } - var confirmed = section.preparedAction - section.disarmAction() - section.actionTriggered(confirmed) - } - } Button { visible: sectionFooter.showOpen text: "Open in GitHub 󰅂" @@ -1151,6 +1152,7 @@ Panel { property bool pulse: false property bool danger: false property bool showReadAction: false + property bool showTrailingIndicator: true property string notificationId: "" property string rowKind: "" property int rowIndex: 0 @@ -1235,7 +1237,13 @@ Panel { onClicked: github.markNotificationRead(linkRow.notificationId) } } - Text { text: "󰅂"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.body } + Text { + visible: linkRow.showTrailingIndicator + text: "󰅂" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.body + } } } diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index 503a3cf..196de57 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -67,6 +67,10 @@ assert_contains $'onPreviousPage: root.notificationsPage = Math.max(0, root.noti "notification page controls do not clamp their range" assert_contains $'model: root.notificationRows()\n showExpansionControl: false\n page: root.notificationsPage' \ "notification pagination still shows an inactive expansion control" +assert_contains $'showReadAction: true\n showTrailingIndicator: false\n notificationId: String(modelData.id || "")' \ + "notification rows retain an open-link indicator beside their read action" +assert_contains 'readonly property bool showAction: section.count > 0 && section.actionText !== ""' \ + "bulk notification action disappears while data is loading" assert_contains $'text: "󰅁"\n tooltipText: "Previous notifications"' \ "previous notification page control is missing" assert_contains $'text: "󰅂"\n tooltipText: "Next notifications"' \ From 219ee16943e73beefb04412697097d5ec2aa89bf Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 19:38:22 +1000 Subject: [PATCH 06/13] fix(notifications): retain footer button borders Keep notification bulk and open controls visually consistent with other section footers.\n\nValidation:\n- tests/panel-source-test.sh\n- tests/service-source-test.sh\n- tests/helper-test.sh\n- omarchy plugin validate .\n\nAssisted-by: Codex/GPT-5 --- Panel.qml | 6 ++++-- tests/panel-source-test.sh | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Panel.qml b/Panel.qml index 2503ed5..be8563d 100644 --- a/Panel.qml +++ b/Panel.qml @@ -469,6 +469,7 @@ Panel { emptyText: github.state === "ready" ? "You're all caught up." : "No notifications loaded." model: root.notificationRows() showExpansionControl: false + footerButtonsBordered: true page: root.notificationsPage pageCount: root.notificationPageCount() openUrl: "https://github.com/notifications" @@ -983,6 +984,7 @@ Panel { property Component delegateComponent: null property bool expanded: false property bool showExpansionControl: true + property bool footerButtonsBordered: false property string openUrl: "" property int page: 0 property int pageCount: 1 @@ -1076,7 +1078,7 @@ Panel { visible: sectionFooter.showAction enabled: section.actionEnabled && !section.actionBusy text: section.actionBusy ? section.actionBusyText : (section.actionArmed ? section.actionConfirmText : section.actionText) - bordered: sectionFooter.expandable + bordered: sectionFooter.expandable || section.footerButtonsBordered foreground: section.actionArmed ? root.urgent : root.foreground fontFamily: root.fontFamily fontSize: Style.font.caption @@ -1133,7 +1135,7 @@ Panel { Button { visible: sectionFooter.showOpen text: "Open in GitHub 󰅂" - bordered: sectionFooter.expandable + bordered: sectionFooter.expandable || section.footerButtonsBordered foreground: root.foreground fontFamily: root.fontFamily fontSize: Style.font.caption diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index 196de57..4cfcb14 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -65,7 +65,7 @@ assert_contains $'function notificationRows() {\n var page = Math.max(0, Math "notifications are not paged in five-item windows" assert_contains $'onPreviousPage: root.notificationsPage = Math.max(0, root.notificationsPage - 1)\n onNextPage: root.notificationsPage = Math.min(root.notificationPageCount() - 1, root.notificationsPage + 1)' \ "notification page controls do not clamp their range" -assert_contains $'model: root.notificationRows()\n showExpansionControl: false\n page: root.notificationsPage' \ +assert_contains $'model: root.notificationRows()\n showExpansionControl: false\n footerButtonsBordered: true\n page: root.notificationsPage' \ "notification pagination still shows an inactive expansion control" assert_contains $'showReadAction: true\n showTrailingIndicator: false\n notificationId: String(modelData.id || "")' \ "notification rows retain an open-link indicator beside their read action" From cc08d67985feef7feaaee07715756b64dfd2d92b Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 19:47:07 +1000 Subject: [PATCH 07/13] fix(notifications): use standard tick background Use a transparent idle background for notification read controls to match other buttons.\n\nValidation:\n- tests/panel-source-test.sh\n- tests/service-source-test.sh\n- tests/helper-test.sh\n- omarchy plugin validate .\n\nAssisted-by: Codex/GPT-5 --- Panel.qml | 2 +- tests/panel-source-test.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Panel.qml b/Panel.qml index be8563d..f54aa50 100644 --- a/Panel.qml +++ b/Panel.qml @@ -1222,7 +1222,7 @@ Panel { implicitWidth: readAction.size implicitHeight: readAction.size radius: Style.cornerRadius - color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.06) + color: "transparent" borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.16), 1) PanelActionButton { diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index 4cfcb14..e0b77fd 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -59,8 +59,8 @@ assert_contains $'pageFlip.stop()\n settingsOpen = false' \ "closing the panel leaves it on the settings page" assert_contains $'BorderSurface {\n visible: linkRow.showReadAction\n Layout.alignment: Qt.AlignVCenter\n Layout.fillHeight: true' \ "notification row marking is disabled during refresh" -assert_contains $'color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.06)\n borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.16), 1)' \ - "notification read action is not subtly distinct" +assert_contains $'color: "transparent"\n borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.16), 1)' \ + "notification read action does not use the standard transparent background" assert_contains $'function notificationRows() {\n var page = Math.max(0, Math.min(notificationsPage, notificationPageCount() - 1))' \ "notifications are not paged in five-item windows" assert_contains $'onPreviousPage: root.notificationsPage = Math.max(0, root.notificationsPage - 1)\n onNextPage: root.notificationsPage = Math.min(root.notificationPageCount() - 1, root.notificationsPage + 1)' \ From e80f0996f66841d49d09da527a4b32af6a0fd971 Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 19:55:06 +1000 Subject: [PATCH 08/13] feat(notifications): integrate row read action Make the notification read target a full-height trailing strip with only a left divider.\n\nValidation:\n- tests/panel-source-test.sh\n- tests/service-source-test.sh\n- tests/helper-test.sh\n- omarchy plugin validate .\n\nAssisted-by: Codex/GPT-5 --- Panel.qml | 23 +++++++++++++++-------- tests/panel-source-test.sh | 8 ++++++-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Panel.qml b/Panel.qml index f54aa50..ca7ddfd 100644 --- a/Panel.qml +++ b/Panel.qml @@ -1178,7 +1178,7 @@ Panel { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: Style.space(9) - anchors.rightMargin: Style.space(9) + anchors.rightMargin: linkRow.showReadAction ? 0 : Style.space(9) spacing: Style.space(9) Text { text: linkRow.glyph @@ -1219,23 +1219,30 @@ Panel { visible: linkRow.showReadAction Layout.alignment: Qt.AlignVCenter Layout.fillHeight: true - implicitWidth: readAction.size - implicitHeight: readAction.size - radius: Style.cornerRadius + implicitWidth: Style.space(32) + implicitHeight: Style.space(32) + radius: 0 color: "transparent" - borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.16), 1) + borderSpec: Border.none() + + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: Style.normalBorderWidth + color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.16) + } PanelActionButton { id: readAction - anchors.centerIn: parent + anchors.fill: parent enabled: github.markingNotificationId !== linkRow.notificationId iconText: github.markingNotificationId === linkRow.notificationId ? "󰑐" : "󰄬" tooltipText: "Mark this notification read (M)" foreground: root.foreground hoverColor: Color.accent fontFamily: root.fontFamily - size: Math.max(Style.space(32), fontSize + Style.spacing.sm * 2) - bordered: true + bordered: false onClicked: github.markNotificationRead(linkRow.notificationId) } } diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index e0b77fd..a55ada6 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -59,8 +59,12 @@ assert_contains $'pageFlip.stop()\n settingsOpen = false' \ "closing the panel leaves it on the settings page" assert_contains $'BorderSurface {\n visible: linkRow.showReadAction\n Layout.alignment: Qt.AlignVCenter\n Layout.fillHeight: true' \ "notification row marking is disabled during refresh" -assert_contains $'color: "transparent"\n borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.16), 1)' \ - "notification read action does not use the standard transparent background" +assert_contains $'anchors.rightMargin: linkRow.showReadAction ? 0 : Style.space(9)' \ + "notification read target leaves dead space at the row edge" +assert_contains $'borderSpec: Border.none()\n\n Rectangle {\n anchors.left: parent.left\n anchors.top: parent.top\n anchors.bottom: parent.bottom' \ + "notification read target does not use a left-only divider" +assert_contains $'anchors.fill: parent\n enabled: github.markingNotificationId !== linkRow.notificationId' \ + "notification read target does not fill its action strip" assert_contains $'function notificationRows() {\n var page = Math.max(0, Math.min(notificationsPage, notificationPageCount() - 1))' \ "notifications are not paged in five-item windows" assert_contains $'onPreviousPage: root.notificationsPage = Math.max(0, root.notificationsPage - 1)\n onNextPage: root.notificationsPage = Math.min(root.notificationPageCount() - 1, root.notificationsPage + 1)' \ From 2991c84762a2d61f99ede289477d6165fec941e0 Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 20:11:26 +1000 Subject: [PATCH 09/13] fix(notifications): restore bulk action gating Match upstream bulk-read control readiness behavior while retaining notification pagination.\n\nValidation:\n- tests/panel-source-test.sh\n- tests/service-source-test.sh\n- tests/helper-test.sh\n- omarchy plugin validate .\n\nAssisted-by: Codex/GPT-5 --- Panel.qml | 4 ++-- tests/panel-source-test.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Panel.qml b/Panel.qml index ca7ddfd..15c6971 100644 --- a/Panel.qml +++ b/Panel.qml @@ -1053,7 +1053,7 @@ Panel { readonly property bool expandable: section.showExpansionControl && section.count > root.activityPreviewCount readonly property bool paginated: section.pageCount > 1 readonly property bool showOpen: section.count > 0 && section.openUrl !== "" - readonly property bool showAction: section.count > 0 && section.actionText !== "" + readonly property bool showAction: section.count > 0 && section.actionEnabled && section.actionText !== "" visible: expandable || paginated || showOpen || showAction anchors.horizontalCenter: parent.horizontalCenter spacing: Style.space(12) @@ -1076,7 +1076,7 @@ Panel { onImplicitWidthChanged: reservedWidth = Math.max(reservedWidth, implicitWidth) width: Math.max(reservedWidth, implicitWidth) visible: sectionFooter.showAction - enabled: section.actionEnabled && !section.actionBusy + enabled: !section.actionBusy text: section.actionBusy ? section.actionBusyText : (section.actionArmed ? section.actionConfirmText : section.actionText) bordered: sectionFooter.expandable || section.footerButtonsBordered foreground: section.actionArmed ? root.urgent : root.foreground diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index a55ada6..2c63a08 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -73,8 +73,8 @@ assert_contains $'model: root.notificationRows()\n showExpansionContr "notification pagination still shows an inactive expansion control" assert_contains $'showReadAction: true\n showTrailingIndicator: false\n notificationId: String(modelData.id || "")' \ "notification rows retain an open-link indicator beside their read action" -assert_contains 'readonly property bool showAction: section.count > 0 && section.actionText !== ""' \ - "bulk notification action disappears while data is loading" +assert_contains 'readonly property bool showAction: section.count > 0 && section.actionEnabled && section.actionText !== ""' \ + "bulk notification action no longer follows upstream readiness behavior" assert_contains $'text: "󰅁"\n tooltipText: "Previous notifications"' \ "previous notification page control is missing" assert_contains $'text: "󰅂"\n tooltipText: "Next notifications"' \ From 00f0e1fd6c0858350c708e0740bc2dc9d9817e4a Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 20:35:13 +1000 Subject: [PATCH 10/13] fix(notifications): restore bulk action and row target Keep Mark all read visible until notifications are ready, and make the per-row read action a full-height attached strip. Also retain the matching bordered Open in GitHub control for assigned issues. Validation: - tests/panel-source-test.sh - tests/service-source-test.sh - tests/helper-test.sh - omarchy plugin validate . Assisted-by: Codex/GPT-5 --- Panel.qml | 72 ++++++++++++++++++++------------------ tests/panel-source-test.sh | 20 ++++++----- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/Panel.qml b/Panel.qml index 15c6971..9f7fed7 100644 --- a/Panel.qml +++ b/Panel.qml @@ -516,6 +516,7 @@ Panel { count: github.assignedIssues.length model: root.sectionRows(github.assignedIssues, root.issuesExpanded) expanded: root.issuesExpanded + footerButtonsBordered: true openUrl: "https://github.com/issues/assigned" onToggleExpanded: root.issuesExpanded = !root.issuesExpanded delegateComponent: issueDelegate @@ -1053,7 +1054,7 @@ Panel { readonly property bool expandable: section.showExpansionControl && section.count > root.activityPreviewCount readonly property bool paginated: section.pageCount > 1 readonly property bool showOpen: section.count > 0 && section.openUrl !== "" - readonly property bool showAction: section.count > 0 && section.actionEnabled && section.actionText !== "" + readonly property bool showAction: section.count > 0 && section.actionText !== "" visible: expandable || paginated || showOpen || showAction anchors.horizontalCenter: parent.horizontalCenter spacing: Style.space(12) @@ -1076,7 +1077,7 @@ Panel { onImplicitWidthChanged: reservedWidth = Math.max(reservedWidth, implicitWidth) width: Math.max(reservedWidth, implicitWidth) visible: sectionFooter.showAction - enabled: !section.actionBusy + enabled: section.actionEnabled && !section.actionBusy text: section.actionBusy ? section.actionBusyText : (section.actionArmed ? section.actionConfirmText : section.actionText) bordered: sectionFooter.expandable || section.footerButtonsBordered foreground: section.actionArmed ? root.urgent : root.foreground @@ -1175,10 +1176,10 @@ Panel { RowLayout { id: row anchors.left: parent.left - anchors.right: parent.right + anchors.right: readActionStrip.visible ? readActionStrip.left : parent.right anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: Style.space(9) - anchors.rightMargin: linkRow.showReadAction ? 0 : Style.space(9) + anchors.rightMargin: readActionStrip.visible ? 0 : Style.space(9) spacing: Style.space(9) Text { text: linkRow.glyph @@ -1215,37 +1216,6 @@ Panel { elide: Text.ElideRight } } - BorderSurface { - visible: linkRow.showReadAction - Layout.alignment: Qt.AlignVCenter - Layout.fillHeight: true - implicitWidth: Style.space(32) - implicitHeight: Style.space(32) - radius: 0 - color: "transparent" - borderSpec: Border.none() - - Rectangle { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - width: Style.normalBorderWidth - color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.16) - } - - PanelActionButton { - id: readAction - anchors.fill: parent - enabled: github.markingNotificationId !== linkRow.notificationId - iconText: github.markingNotificationId === linkRow.notificationId ? "󰑐" : "󰄬" - tooltipText: "Mark this notification read (M)" - foreground: root.foreground - hoverColor: Color.accent - fontFamily: root.fontFamily - bordered: false - onClicked: github.markNotificationRead(linkRow.notificationId) - } - } Text { visible: linkRow.showTrailingIndicator text: "󰅂" @@ -1254,6 +1224,38 @@ Panel { font.pixelSize: Style.font.body } } + BorderSurface { + id: readActionStrip + visible: linkRow.showReadAction + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + width: Style.space(32) + radius: 0 + color: "transparent" + borderSpec: Border.none() + + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: Style.normalBorderWidth + color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.16) + } + + PanelActionButton { + id: readAction + anchors.fill: parent + enabled: github.markingNotificationId !== linkRow.notificationId + iconText: github.markingNotificationId === linkRow.notificationId ? "󰑐" : "󰄬" + tooltipText: "Mark this notification read (M)" + foreground: root.foreground + hoverColor: Color.accent + fontFamily: root.fontFamily + bordered: false + onClicked: github.markNotificationRead(linkRow.notificationId) + } + } } component RepoRow: CursorSurface { diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index 2c63a08..0a7afa5 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -57,13 +57,13 @@ assert_contains 'visible: root.settingsOpen' \ "the settings page is always visible" assert_contains $'pageFlip.stop()\n settingsOpen = false' \ "closing the panel leaves it on the settings page" -assert_contains $'BorderSurface {\n visible: linkRow.showReadAction\n Layout.alignment: Qt.AlignVCenter\n Layout.fillHeight: true' \ - "notification row marking is disabled during refresh" -assert_contains $'anchors.rightMargin: linkRow.showReadAction ? 0 : Style.space(9)' \ - "notification read target leaves dead space at the row edge" -assert_contains $'borderSpec: Border.none()\n\n Rectangle {\n anchors.left: parent.left\n anchors.top: parent.top\n anchors.bottom: parent.bottom' \ +assert_contains $'id: readActionStrip\n visible: linkRow.showReadAction\n anchors.right: parent.right\n anchors.top: parent.top\n anchors.bottom: parent.bottom\n width: Style.space(32)' \ + "notification read target does not fill the row height at its right edge" +assert_contains $'anchors.right: readActionStrip.visible ? readActionStrip.left : parent.right\n anchors.verticalCenter: parent.verticalCenter\n anchors.leftMargin: Style.space(9)\n anchors.rightMargin: readActionStrip.visible ? 0 : Style.space(9)' \ + "notification content does not meet the full-height read target" +assert_contains $'borderSpec: Border.none()\n\n Rectangle {\n anchors.left: parent.left\n anchors.top: parent.top\n anchors.bottom: parent.bottom' \ "notification read target does not use a left-only divider" -assert_contains $'anchors.fill: parent\n enabled: github.markingNotificationId !== linkRow.notificationId' \ +assert_contains $'anchors.fill: parent\n enabled: github.markingNotificationId !== linkRow.notificationId' \ "notification read target does not fill its action strip" assert_contains $'function notificationRows() {\n var page = Math.max(0, Math.min(notificationsPage, notificationPageCount() - 1))' \ "notifications are not paged in five-item windows" @@ -73,8 +73,12 @@ assert_contains $'model: root.notificationRows()\n showExpansionContr "notification pagination still shows an inactive expansion control" assert_contains $'showReadAction: true\n showTrailingIndicator: false\n notificationId: String(modelData.id || "")' \ "notification rows retain an open-link indicator beside their read action" -assert_contains 'readonly property bool showAction: section.count > 0 && section.actionEnabled && section.actionText !== ""' \ - "bulk notification action no longer follows upstream readiness behavior" +assert_contains $'title: "ASSIGNED ISSUES"\n count: github.assignedIssues.length\n model: root.sectionRows(github.assignedIssues, root.issuesExpanded)\n expanded: root.issuesExpanded\n footerButtonsBordered: true\n openUrl: "https://github.com/issues/assigned"' \ + "assigned-issues open control does not retain its matching border" +assert_contains 'readonly property bool showAction: section.count > 0 && section.actionText !== ""' \ + "bulk notification action disappears while data is loading" +assert_contains 'enabled: section.actionEnabled && !section.actionBusy' \ + "bulk notification action is not disabled until it is ready" assert_contains $'text: "󰅁"\n tooltipText: "Previous notifications"' \ "previous notification page control is missing" assert_contains $'text: "󰅂"\n tooltipText: "Next notifications"' \ From f1bc28d6ef4bfbcb0281e5e0971455b13e2f0d2b Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 20:44:06 +1000 Subject: [PATCH 11/13] fix(notifications): sync row hover with read action Selecting the full-height read action now selects its parent row too, keeping hover feedback visually continuous. Validation: - tests/panel-source-test.sh - tests/service-source-test.sh - tests/helper-test.sh - omarchy plugin validate . Assisted-by: Codex/GPT-5 --- Panel.qml | 4 ++++ tests/panel-source-test.sh | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Panel.qml b/Panel.qml index 9f7fed7..cf19e45 100644 --- a/Panel.qml +++ b/Panel.qml @@ -1235,6 +1235,10 @@ Panel { color: "transparent" borderSpec: Border.none() + HoverHandler { + onHoveredChanged: if (hovered) root.selectKey(linkRow.cursorKey) + } + Rectangle { anchors.left: parent.left anchors.top: parent.top diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index 0a7afa5..2f7fb6a 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -61,7 +61,7 @@ assert_contains $'id: readActionStrip\n visible: linkRow.showReadAction\n "notification read target does not fill the row height at its right edge" assert_contains $'anchors.right: readActionStrip.visible ? readActionStrip.left : parent.right\n anchors.verticalCenter: parent.verticalCenter\n anchors.leftMargin: Style.space(9)\n anchors.rightMargin: readActionStrip.visible ? 0 : Style.space(9)' \ "notification content does not meet the full-height read target" -assert_contains $'borderSpec: Border.none()\n\n Rectangle {\n anchors.left: parent.left\n anchors.top: parent.top\n anchors.bottom: parent.bottom' \ +assert_contains $'borderSpec: Border.none()\n\n HoverHandler {\n onHoveredChanged: if (hovered) root.selectKey(linkRow.cursorKey)\n }\n\n Rectangle {\n anchors.left: parent.left\n anchors.top: parent.top\n anchors.bottom: parent.bottom' \ "notification read target does not use a left-only divider" assert_contains $'anchors.fill: parent\n enabled: github.markingNotificationId !== linkRow.notificationId' \ "notification read target does not fill its action strip" From 90062f9f35824beb6d7c1901e356e3a1ff5e813e Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 20:45:07 +1000 Subject: [PATCH 12/13] fix(notifications): show bulk read progress immediately Optimistically clear notifications during bulk marking, restore them on failure, and retain the authoritative refresh after completion. Validation: - tests/service-source-test.sh - tests/panel-source-test.sh - tests/helper-test.sh - omarchy plugin validate . Assisted-by: Codex/GPT-5 --- Service.qml | 25 ++++++++++++++++++++++++- tests/service-source-test.sh | 6 ++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/Service.qml b/Service.qml index 9fe1315..5bb03cc 100644 --- a/Service.qml +++ b/Service.qml @@ -31,6 +31,7 @@ Item { property bool refreshQueued: false property string markingNotificationId: "" property bool markingAllNotifications: false + property var markingAllNotificationIds: [] property string notificationActionStatus: "" property string _markStdout: "" property string _markStderr: "" @@ -171,6 +172,24 @@ Item { notificationsRevision++; } + function hideAllNotifications() { + var ids = []; + for (var i = 0; i < notifications.length; i++) { + var id = String(notifications[i].id || ""); + if (id !== "") + ids.push(id); + } + for (var j = 0; j < ids.length; j++) + hideNotification(ids[j]); + return ids; + } + + function restoreHiddenNotifications(ids) { + var values = Array.isArray(ids) ? ids : []; + for (var i = 0; i < values.length; i++) + restoreHiddenNotification(values[i]); + } + function visibleNotifications(rows) { var incoming = Array.isArray(rows) ? rows : []; var hidden = hiddenNotifications || {}; @@ -341,6 +360,7 @@ Item { actionStatusTimer.stop(); markingAllNotifications = true; + markingAllNotificationIds = hideAllNotifications(); notificationActionStatus = "Marking all notifications read…"; _markStdout = ""; _markStderr = ""; @@ -427,11 +447,14 @@ Item { } else { var fallback = all ? "Could not mark all notifications read." : "Could not mark notification read."; root.notificationActionStatus = response && response.message ? String(response.message) : String(markErrors.text || root._markStderr || fallback).trim(); - if (!all && markedId !== "") + if (all) + root.restoreHiddenNotifications(root.markingAllNotificationIds); + else if (markedId !== "") root.restoreHiddenNotification(markedId); } root.markingNotificationId = ""; root.markingAllNotifications = false; + root.markingAllNotificationIds = []; actionStatusTimer.restart(); if (root.startQueuedMark()) return ; diff --git a/tests/service-source-test.sh b/tests/service-source-test.sh index 74312ff..5002062 100755 --- a/tests/service-source-test.sh +++ b/tests/service-source-test.sh @@ -50,6 +50,12 @@ assert_contains $'if (confirmed !== prepareMarkAllNotificationsRead()) {\n "bulk marking does not verify the confirmed snapshot" assert_contains $'var commandLine = [helperPath(), "--mark-all-read-before", String(snapshot.boundary || "")];\n for (var i = 0; i < snapshot.boundaryIds.length; i++)\n commandLine.push("--mark-boundary-notification", String(snapshot.boundaryIds[i]));' \ "bulk marking does not protect same-second arrivals" +assert_contains $'function hideAllNotifications() {\n var ids = [];\n for (var i = 0; i < notifications.length; i++) {' \ + "bulk marking does not immediately clear its displayed notifications" +assert_contains $'markingAllNotifications = true;\n markingAllNotificationIds = hideAllNotifications();\n notificationActionStatus = "Marking all notifications read…";' \ + "bulk marking does not provide immediate visible feedback" +assert_contains $'if (all)\n root.restoreHiddenNotifications(root.markingAllNotificationIds);\n else if (markedId !== "")' \ + "failed bulk marking does not restore its displayed notifications" assert_contains $'// GitHub is authoritative after every attempt. This reconciles\n // successful, failed, and partially completed bulk operations.\n root.refreshQueued = false;\n Qt.callLater(root.refresh);' \ "notification marking does not reconcile every result with an authoritative refresh" assert_not_contains 'root.notifications = root.notifications.filter' \ From dfe5337d3329655d3c3ef5a4a26ae898a16beba5 Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Mon, 24 Aug 2026 22:47:26 +1000 Subject: [PATCH 13/13] fix(notifications): batch bulk read reconciliation Clear bulk-marked notifications in one model update and restore failed operations in their original order. Validation: - tests/service-source-test.sh - tests/panel-source-test.sh - tests/helper-test.sh - omarchy plugin validate . Assisted-by: Codex/GPT-5 --- Service.qml | 20 +++++++++++++++----- tests/service-source-test.sh | 8 ++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Service.qml b/Service.qml index 5bb03cc..7a5ed70 100644 --- a/Service.qml +++ b/Service.qml @@ -174,19 +174,29 @@ Item { function hideAllNotifications() { var ids = []; + var hidden = copyMap(hiddenNotifications); + var remaining = []; for (var i = 0; i < notifications.length; i++) { - var id = String(notifications[i].id || ""); - if (id !== "") + var item = notifications[i]; + var id = String(item.id || ""); + if (id !== "") { ids.push(id); + hidden[id] = item; + } else { + remaining.push(item); + } + } + if (ids.length > 0) { + hiddenNotifications = hidden; + notifications = remaining; + notificationsRevision++; } - for (var j = 0; j < ids.length; j++) - hideNotification(ids[j]); return ids; } function restoreHiddenNotifications(ids) { var values = Array.isArray(ids) ? ids : []; - for (var i = 0; i < values.length; i++) + for (var i = values.length - 1; i >= 0; i--) restoreHiddenNotification(values[i]); } diff --git a/tests/service-source-test.sh b/tests/service-source-test.sh index 5002062..00faac0 100755 --- a/tests/service-source-test.sh +++ b/tests/service-source-test.sh @@ -50,8 +50,12 @@ assert_contains $'if (confirmed !== prepareMarkAllNotificationsRead()) {\n "bulk marking does not verify the confirmed snapshot" assert_contains $'var commandLine = [helperPath(), "--mark-all-read-before", String(snapshot.boundary || "")];\n for (var i = 0; i < snapshot.boundaryIds.length; i++)\n commandLine.push("--mark-boundary-notification", String(snapshot.boundaryIds[i]));' \ "bulk marking does not protect same-second arrivals" -assert_contains $'function hideAllNotifications() {\n var ids = [];\n for (var i = 0; i < notifications.length; i++) {' \ - "bulk marking does not immediately clear its displayed notifications" +assert_contains $'function hideAllNotifications() {\n var ids = [];\n var hidden = copyMap(hiddenNotifications);\n var remaining = [];\n for (var i = 0; i < notifications.length; i++) {' \ + "bulk marking does not batch its optimistic removal" +assert_contains $'hiddenNotifications = hidden;\n notifications = remaining;\n notificationsRevision++;' \ + "bulk marking repeatedly updates the notification model" +assert_contains $'function restoreHiddenNotifications(ids) {\n var values = Array.isArray(ids) ? ids : [];\n for (var i = values.length - 1; i >= 0; i--)' \ + "failed bulk marking does not preserve notification order" assert_contains $'markingAllNotifications = true;\n markingAllNotificationIds = hideAllNotifications();\n notificationActionStatus = "Marking all notifications read…";' \ "bulk marking does not provide immediate visible feedback" assert_contains $'if (all)\n root.restoreHiddenNotifications(root.markingAllNotificationIds);\n else if (markedId !== "")' \