WIP: Pilot safe OpenCode pull request reviews #1
Loading…
Reference in a new issue
No description provided.
Delete branch "issue/109-opencode-review-bot-pilot"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Testing
/oc review
OpenCode Review
Execution completed for
lambadalambda/pleromanetat1a043729493a(job #6).Findings
Medium
Unbounded growth of status-engagement tracking maps —
src/routes/app/[...path]/+page.svelteThe new revision-tracking structures (
statusEngagementSnapshots,statusEngagementNotificationRevisions,statusEngagementNotificationArrivals,notificationEngagementSignatures,seenNotificationEngagementSignatures) are keyed by status/notification IDs and never pruned.invalidateStatusActionRequestsresets them only on session/route invalidation, so a long-running session that views many posts will accumulate entries indefinitely. Consider evicting entries for status IDs no longer present in any loaded surface, or limiting to the most-recent N targets. This is the most concrete memory-growth concern introduced by the count-reconciliation feature.Floating image preview is repositioned on every scroll/resize without throttling —
src/lib/rebuild/CompactMediaPreview.sveltepositionFullPhotois bound towindow.scroll(capture),window.resize,visualViewport.resize, andvisualViewport.scroll, plus imageonload. Each invocation performsgetBoundingClientRect()reads, multipleawait tick()calls, and updatesfullPhotoStyle. There is no throttle, debounce, orrequestAnimationFramecoalescing. On lower-end devices and long hover sessions this can introduce jank, especially because the effect tears down and re-creates listeners on everyfullPhoto/fullPhotoAnchorchange. Consider anrAF-batched reposition and keeping listeners attached for the lifetime of the open preview (they are already gated by the open state).Low
Misleading time-bucket labels in NotificationsPage —
src/lib/rebuild/NotificationsPage.sveltebucketFormaps ages 1–6 h to "Today", 6–48 h to "Yesterday", and 2–7 days to "This week". The labels do not match the implied semantics and will be confusing for users (e.g., a 7-hour-old notification shows under "Yesterday"). Either rename the buckets (e.g., "Recent", "Today", "Earlier this week") or compute them against calendar days.Redundant condition in WebSocket onerror handler —
src/lib/pleroma/streaming.tsThe inner
if (!closed)is unreachable becauseclosedcannot change synchronously between the two checks. Collapse to a singleonError?.(event);after the early return.role="note"is non-standard —src/lib/rebuild/CompactMediaPreview.sveltenoteis not in the WAI-ARIA 1.2 vocabulary; some screen readers will ignore it. Use a standard role such asstatus, or omitroleand rely on the visible text.Notification actionable row loses pointer affordance —
src/app.css,src/lib/rebuild/NotifRow.svelteThe old rule
.notif-row.actionable { cursor: pointer; }was removed and the actionable surface is now a real<button class="notif-row-text">. Browser default cursor for<button>isdefault, so the row no longer signals clickability. Addcursor: pointerto.notif-row-text:is(button)(or rely on the existing.notif-quote-open:is(button)pattern).Two distinct buttons open the same notification —
src/lib/rebuild/NotifRow.svelteFor actionable rows with a post, both
.notif-row-text(the whole row) and.notif-quote-open(the excerpt) callonOpen?.(n). This creates redundant focusable targets and a duplicated "open" action in the a11y tree. Either expose a single activator or mark the secondary onetabindex="-1"/aria-hidden.Double notification load after clear —
src/routes/app/[...path]/+page.svelteclearNotificationsfirst runsloadNotifications(session, { replace: true })and then, infinally, unconditionally callsloadNotifications(session, { background: true }). The replace load already fetches authoritative state right after the clear; the second call is a duplicate network request in the common success path. Gate the background follow-up on a real signal (e.g., the replacement stream has opened with no fresh data) or skip it when the replace succeeded.CompactMediaPreviewportal lifecycle is fragile —src/lib/rebuild/CompactMediaPreview.svelteThe
portalaction appendsnodetodocument.bodyand removes it on destroy. Svelte also tries to remove the node from its (now-moved) parent during{#if fullPhoto}teardown. The current ordering works today, but it depends on Svelte calling the actiondestroybefore attempting DOM removal. Adding a comment or guarding the destroy to only remove when still attached would future-proof this against Svelte internals changes.aria-labelon visibility span may double-read —src/lib/rebuild/PostVisibility.svelteThe outer
<span>carriesaria-labelwhile wrapping anIconplus a visible<span>{details.label}</span>. IfIconexposes its own accessible name, AT users may hear the label twice. Ensure the innerIconisaria-hidden="true"(the component is not shown in the patch).Informational
timeline-catch-up.tsandreconcileTimelineCatchUpare intricate but accompanied by a substantial, focused Playwright suite covering multi-page catch-up, concurrent streamed posts, empty-timeline recovery, route retention, and stale local vs. federated races. The behavior matches the tests; future maintainers will benefit from inline comments explaining theinitialDataIds/initialQueuedIdsoverlap algorithm.notificationStreamGeneration, gatesapplyStreamedNotificationon both stream generation andnotificationClearState === 'clearing', and re-checksisCurrentSessionRequestafter eachawait. The in-flight-notification and delayed-stream tests exercise the important races.statusReplyTargetId/matchesStatusReplyTargetcorrectly route boosted-source identity through thread reconciliation, ancestor replies, and reply-target expansion. IndexingbyIdby bothidandstatusReplyTargetIdis safe under the assumption that source IDs are unique within a thread context.compactExcerptshortcode-boundary logic is exported and unit-tested, and is reused for chat excerpts after send — a nice consolidation of the priorchatExcerpthelper.openPleromaTimelineStreamchanges (open-timeout, detach-on-close, idempotent close) are well-covered by the rewritten client tests, including the new "socket never opens" replacement case.Overall
A large, well-structured patch that delivers a coherent set of features (fit-images, compact media previews with floating full-image view, notifications-page redesign, visibility badges, timeline stream catch-up, thread loading/scroll/restoration, count reconciliation, custom-emoji audit fixes, clear-all with generation-safe catch-up). Test coverage is thorough and traces cleanly to the per-issue acceptance criteria. No security, data-integrity, or correctness defects were identified. The most actionable items are the unbounded engagement-snapshot maps (Medium), the unthrottled floating-preview positioning (Medium), and a handful of low-severity UX/a11y/style nits listed above.
/oc rerun
/oc rerun
/oc rerun
/oc rerun
/oc rerun
/oc rerun
OpenCode Review (rerun)
Rerun for
/oc rerun(comment 117412). Reviewedmaster@81c7586→issue/109-opencode-review-bot-pilot@1a04372(21 commits, 64 files, +5417/−333).Verification method: static review of the full base-to-head diff. Tests were not executed in this run (sandbox has node 22; the project requires node >=24 and Playwright/docker integration instances), so runtime findings below are code-traced, not reproduced.
Findings
High
Load-more pages appended during catch-up are misclassified as newer posts, corrupting timeline ordering —
src/lib/pleroma/timeline-state.ts(reconcileTimelineCatchUp) +src/routes/app/[...path]/+page.sveltereconcileTimelineCatchUpassumes every item indatathat is not ininitialDataIdsis newer than the catch-up baseline:But
loadMoreHomeTimelineis not gated on the catch-up mutex (it only checksloadMoreStatus === 'loading') and appends older posts to the end ofdata(data: mergeTimelineItems(homeTimelineState.data, posts)). Those appended posts land inconcurrentData, which (a) forces the insert branch —if (insertImmediately || overlapsConcurrentData || concurrentData.length > 0)— silently flushing the "N new posts" pill into the timeline even wheninsertImmediatelyis false, and (b) places them insideorderedNew, which is prepended above the baseline: older pages render above the baseline and above genuinely new posts. The same applies to the public-timeline variants. Suggested fix: suppress load-more while a catch-up is in flight, or makeconcurrentDataposition-aware (items above the baseline index only).Medium
Overlap branch flushes queued pill posts even when insertion is declined —
src/lib/pleroma/timeline-state.ts(reconcileTimelineCatchUp, overlap branch)In the overlap branch's non-insert path,
queuedBeforeCatchUp(posts sitting unseen in the "N new posts" pill before catch-up started) are merged directly into rebuiltdata:and are excluded from the returned
newerPosts. In the symmetric no-overlap branch the same posts correctly stay innewerPostswhen not inserting. One of the two behaviors must violate theinsertImmediatelycontract; there is no e2e covering overlap + pending pill + auto-insert off.Low
timeline-state.ts:const overlapsConcurrentData = concurrentData.some((item) => incomingIds.has(item.id))only executes whenoverlappingDataIndex === -1(no element ofdatais inincomingIds), so it can never be true. The guard reduces toinsertImmediately || concurrentData.length > 0; the line misleads readers about where concurrent overlap is handled.timeline-catch-up.ts: withsinceIdset, Mastodon-style APIs return anextlink on every non-empty page, so after the last full page the loop always issues one extra (empty) request; termination relies on the server omittingnextfor empty pages, with the repeated-cursor throw as the only backstop. Breaking onpage.items.length === 0would bound it deterministically.+page.svelte(checkHomeTimelineForNewPosts, and the public variant):homeTimelineFallbackSinceId = catchUp.newestId ?? ...runs before thestatus === 'empty' | 'success'dispatch. If a concurrent reload leaves the state'loading', the catch-up result is discarded while the watermark already advanced past those posts (benign today because a successful reload overwrites both, but the invariant would be airtight if moved inside the branches).CompactMediaPreview.svelte: (1)handlePhotoKeydowncallsevent.stopPropagation()on Escape unconditionally, even when no preview is open — swallows the single keyboard path for closing the header notifications popover when focus is on a trigger; early-return whenfullPhotois null (asEmojiPicker.svelte/FocusedPost.sveltedo). (2) The video-sampling action guardsif (!src || node.src) return;and the{#each}is unkeyed, so a refresh that swaps a video attachment keeps sampling the old source and the stickyreadyclass hides fallback text prematurely. (3) The floating full-image preview has no touch dismissal (closes only on mouseleave/blur/Escape).+page.svelte:statusEngagementSnapshots,statusEngagementNotificationArrivals, andnotificationEngagementSignatures/seenNotificationEngagementSignaturesare cleared only on session invalidation, while the persistent app shell keeps adding an entry per loaded status/notification. A slow leak, bounded by distinct ids.+page.svelte: bookmarks fav/boost errors are recorded under route'bookmarks'but no error UI exists there (silent rollback); the reaction picker, poll voting, and inline reply on bookmarks still pass'home'as origin, so a picked reaction toggles scope'home'and replies set state that never renders. Largely pre-existing (everything was'home'before), but the new error plumbing makes the gap visible.Done well
openTimeoutMspath cleans up its timer and detaches handlers, and the generation counter correctly invalidates superseded streams.sinceIdso the multi-page walk cannot cross its window.{@html}anywhere; all user content flows through escaped interpolation orRichText. Sensitive-media previews re-validate against current props before showing originals.NotifRow(genuine buttons replacing therole="button"div) andNotificationsPage(tabpanel, labelled controls); loading states gainedrole="status"/aria-live.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.