Skip to content

feat(mobile): PR review discussion UX + toast entry feedback - #4883

Merged
iscekic merged 12 commits into
mainfrom
pr-review-ux-7f22
Jul 30, 2026
Merged

feat(mobile): PR review discussion UX + toast entry feedback#4883
iscekic merged 12 commits into
mainfrom
pr-review-ux-7f22

Conversation

@iscekic

@iscekic iscekic commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Four PR-review UX improvements on the mobile app, plus the one backend field they
needed.

What: Discussion threads quote the diff lines a comment is anchored to (syntax
highlighting and line numbers, capped at the last 30 lines so the anchored line
always renders); reactions collapse to non-zero pills behind a single add-reaction
icon with a bottom-sheet emoji picker; a resolved thread shows exactly one
"Resolved" indicator (the badge — the toggle is icon-only); and PR-link entry
feedback (empty clipboard, invalid link — typed or pasted) surfaces as toasts.

Why: Reviewing on mobile lacked the context GitHub's own clients show: threads
displayed only a text anchor label, all eight reaction emoji rendered on every
comment (including zero-count ones), "Resolved" rendered twice per resolved thread,
and the entry screen's inline helper messages shifted layout and competed with the
input.

How: The GraphQL review-threads query now selects diffHunk on each thread's
first comment, carried through the DTO as a required-nullable field ('' → null).
The app parses it with the existing parsePatch and draws it with the Files tab's
DiffLine, only when the thread is expanded (memoized); outdated threads show the
original hunk (GitHub semantics), file-level threads degrade to the label only.
Reactions filter to count > 0 via a tested pure selector, with a new
readOnly/disabled split so conversation comments stay read-only while pending
mutations merely lock presses. Entry feedback uses the app's existing
toast.error pattern — an earlier iteration of this PR reserved an inline slot for
the messages; per the requester's direction the inline helper UI was removed
entirely (the helper-slot lib is deleted, surviving pieces moved to
pr-link-paste.ts), so the layout has no message row and cannot shift. The E2E
GitHub stub became stateful (resolve/unresolve/reactions) with per-platform
fixtures so iOS and Android verifiers run in parallel without racing.

Verification

  • E2E on both platforms (real stack + hermetic GitHub stub, fixtures
    kilo-stub/discussion-mixed refactor(bot): Extract platform-agnostic tool system #1 iOS / Add env lazy loading for dev environment #11 Android): all four acceptance criteria
    passed at 47ebec33d — iOS full pass (round 2); Android full pass (round 2) plus
    a focused round-3 re-check of the invalid-link layout claim with a
    constant-field-state protocol (a round-2 FAIL on that point was disproven by the
    verifier's own hierarchies: the slot held 46px in every state; the observed
    +14px was the pre-existing clear-button row growth from Mobile review surfaces and invoice download #4776, present on main).
    Evidence: screenshots below and per-case hierarchies/logs retained in the run
    record.
  • Final toast delta (0bc3f1bba, invalid-link → toast + helper-slot
    removal): deliberately NOT re-run through E2E per the requester's call — the
    toast mechanism and paste state machine are the exact path the E2E rounds
    live-verified for clipboard-empty (screenshot below), and this delta is
    presentation-only on that mechanism. Covered instead by unit tests plus
    typecheck/lint/format (all green).

Automated coverage at HEAD (0bc3f1bba): backend mapper/router tests for the new
field (139 web tests green, including the GraphQL schema validation of the edited
query); mobile unit tests for the snippet selector, reaction pills, paste/toast
copy, and clear-button selector (full mobile suite, 2470 tests, green); typecheck,
lint, and unused-export checks green on both packages; root format clean.

Visual Changes

After shots are from the E2E verification runs (iOS). The AC4 "before" is the
inline helper message from the first E2E round, before the toast scope change.

Item Before After
Anchored diff hunk in thread No snippet rendered (anchor label only) ac1-01-thread-a.png
Collapsed reactions All 8 emoji pills on every comment ac2-01-thread-a-pills.png
Add-reaction picker (none) ac2-02-picker.png
Single resolved indicator "Resolved" rendered twice ac3-01-resolved-badge.png
Entry feedback as toast 17-clipboard-empty-crop.png ac4-02-toast-visible.png

Reviewer Notes

  • github-pr-review-router.ts: only diffHunk was added to the query/type/thread
    builder; CONVERSATION_COMMENTS_QUERY intentionally unchanged (IssueComment has
    no diffHunk).
  • discussion-thread.tsx: the "Resolved" badge is the sole resolved text; the
    toggle kept its accessibility labels. The snippet cap keeps the hunk's TAIL —
    GitHub's diffHunk ends at the anchored line (a Kilobot finding, fixed).
  • server.mjs is committed E2E harness code: buildMixedFixture(idSuffix)
    registers identical fixtures at pulls 1 and 11 with globally unique ids, and the
    new mutations mutate per-key fixture objects in place (process lifetime).
  • The entry screen's clear-button row growth when the field gains content is
    pre-existing (Mobile review surfaces and invoice download #4776 on main) and out of scope here; both verifiers isolated it
    from the helper-message measurements.
  • The invalid-link toast reuses the exact clipboard-empty toast path verified by
    E2E; the E2E skip for that final delta is the requester's documented call.

iscekic added 6 commits July 30, 2026 13:02
Select diffHunk on each thread's first comment in REVIEW_THREADS_QUERY,
carry it through the review-thread DTO (nullable, '' mapped to null),
and cover the mapping with mapper and router tests.
Render the thread's diffHunk (from the first comment) above the comments
when expanded, parsed via parsePatch and drawn with DiffLine, capped at
30 lines with a trailing count row. File-level and malformed anchors fall
back to the label only. Resolve toggle becomes an icon-only button so the
Resolved badge is the single resolved indicator.
Build discussion-mixed via buildMixedFixture(idSuffix) registered at
pulls 1 (iOS) and 11 (Android) with globally unique ids; add diffHunk
strings, reaction-count overrides, outdated/file/resolved threads, and
stateful ResolveThread/UnresolveThread/AddReaction/RemoveReaction
mutations resolved by scanning fixtures for the unique id.
Render only non-zero reaction buckets as pills and add a single
SmilePlus button that opens a bottom-sheet picker with all eight emoji
(Portal name pr-review-reactions, backdrop, BackHandler on Android).
Split readOnly from disabled so conversation comments show read-only
pills with no add icon while pending mutations only lock presses.
Mount one stable Text between the input row and Open button; swap only
its copy (message or non-breaking-space placeholder) and color token so
helper messages appear and clear without moving surrounding controls.
The review-thread DTO gained a required nullable diffHunk; the shared
makeThread helpers now include it so typecheck passes.
@iscekic iscekic self-assigned this Jul 30, 2026
Comment thread apps/mobile/src/lib/pr-review/discussion/thread-diff-snippet.ts Outdated
Comment thread apps/mobile/src/components/pr-review/discussion/discussion-thread.tsx Outdated
Comment thread apps/mobile/src/components/pr-review/pr-review-entry-screen.tsx Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of 0bc3f1bb..a532ae68 (a single 8-line change swapping raw toast.error for announcingToast.error on the PR-link entry screen): the change resolves the previous accessibility warning and introduces no new issues; confidence is high given the tiny, self-contained scope.

Files Reviewed (1 file)
  • apps/mobile/src/components/pr-review/pr-review-entry-screen.tsx
Previous findings — current state
  • Resolved: the WARNING that the entry screen's failure outcomes had no screen-reader feedback after the inline helper slot was deleted. All three failure paths now call announcingToast.error (pr-review-entry-screen.tsx:79, :97, :103), which announces via announceForA11y (AccessibilityInfo.announceForAccessibility) in addition to rendering the visual toast — the documented adapter for consequential outcomes.
  • Earlier findings on thread-diff-snippet.ts, discussion-thread.tsx, and the helper-slot copy were already confirmed fixed or made obsolete in prior increments and are not part of this increment's diff, so they are not carried forward.
Notes and assumptions
  • The sonner-native import was fully removed and no other toast. reference remains in the file, so the swap is complete rather than partial.
  • announcingToast.error returns the same string | number toast id and forwards options, so the call sites keep identical visual behaviour; the added announcement trims/drops empty messages and does not throw when no screen reader is active.
  • No memory-leak-shaped changes in this increment: no new state, subscriptions, timers, listeners, or caches; the announcement is a fire-and-forget platform call.
  • The changed file contains no images or markdown docs, so the markdown-image and content/SEO rules do not apply.
  • PR-body verification claims (E2E on both platforms, the deliberate E2E skip for this toast delta) were not independently checked; this read-only review executed no repository checks, tests, lint, or typecheck.
Previous Review Summaries (3 snapshots, latest commit 0bc3f1b)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 0bc3f1b)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

Incremental review of 47ebec33..0bc3f1bb (inline helper slot deleted, replaced by toasts): the change removes the last accessible surface for the entry screen's failure outcomes, leaving invalid-link and empty-clipboard silent for screen readers on Android.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/components/pr-review/pr-review-entry-screen.tsx 79 All three failure paths (79, 97, 103) now use raw toast.error; with the accessible helper Text gone, invalid-link / empty-clipboard produce no screen-reader feedback. announcingToast.error from @/lib/a11y/announcing-toast is the documented adapter for consequential outcomes.
Files Reviewed (6 files)
  • apps/mobile/src/components/pr-review/pr-review-entry-screen.tsx - 1 issue
  • apps/mobile/src/lib/pr-review/pr-link-paste.ts
  • apps/mobile/src/lib/pr-review/pr-link-paste.test.ts
  • apps/mobile/src/lib/pr-review/pr-link-helper-slot.ts (deleted)
  • apps/mobile/src/lib/pr-review/pr-link-helper-slot.test.ts (deleted)
  • .kilo_workflow/learnings/*.md (3 new docs, documentation only)
Previous findings — current state
  • Resolved (obsolete): the helper-slot numberOfLines={1} / NBSP-announcement finding no longer applies — the entire helper slot, PrLinkHelperMessage state, and pr-link-helper-slot.ts module were deleted in this increment.
  • The two thread-diff-snippet.ts / discussion-thread.tsx findings were already confirmed fixed at 47ebec33 and are untouched here; not re-raised.
Notes and assumptions
  • Deletion of pr-link-helper-slot.ts is clean: no remaining reference anywhere in the repo to that module path or to any PR_LINK_HELPER_* symbol, and the two pinned copy strings now live in pr-link-paste.ts with test coverage, so check:unused should stay green. selectPrLinkClearButtonVisible moved into pr-link-paste.ts and is still consumed at pr-review-entry-screen.tsx:121.
  • No E2E flow or fixture references the removed inline copy, so no test breakage from the removal.
  • Behaviour change not raised as a defect: Open is now disabled={!hasInput} only (previously also disabled while invalid), so a non-PR URL keeps the button pressable and re-toasts on each press. That follows from moving the invalid state out of React state and appears intentional.
  • The stale-helper-after-Clear issue noted in the previous review is now moot — there is no helperMessage state left to leak.
  • No memory-leak-shaped changes in this increment: state and one useState were removed; no new subscriptions, timers, or caches.
  • The three new .kilo_workflow/learnings/*.md files contain no images, so the markdown-image rule does not apply.
  • Verification claims in the PR body (E2E on both platforms, screenshots) were still unchecked at review time; assessment was by code reading only, and no repository checks were executed in this read-only review.

Fix these issues in Kilo Cloud

Previous review (commit 47ebec3)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of dd659d4a..47ebec33 (snippet tail-cap, expanded-only memoized parse, clipboard-empty toast, helper-slot a11y): all three previous findings are addressed and the new changes introduce no defect I can reproduce in the changed lines.

Files Reviewed (9 files)
  • apps/mobile/src/components/pr-review/discussion/discussion-thread.tsx
  • apps/mobile/src/components/pr-review/discussion/thread-diff-snippet.tsx
  • apps/mobile/src/components/pr-review/pr-review-entry-screen.tsx
  • apps/mobile/src/lib/pr-review/discussion/thread-diff-snippet.ts
  • apps/mobile/src/lib/pr-review/discussion/thread-diff-snippet.test.ts
  • apps/mobile/src/lib/pr-review/pr-link-helper-slot.ts
  • apps/mobile/src/lib/pr-review/pr-link-helper-slot.test.ts
  • .kilo_workflow/learnings/mobile-android-a11y-label-leaves-geometric-grouping.md
  • .kilo_workflow/learnings/mobile-e2e-fullstring-regex-selector-escaping.md
  • .kilo_workflow/learnings/mobile-e2e-scrolluntilvisible-lands-but-throws.md
Previous findings — current state
  • Fixed: thread-diff-snippet.ts:56 now uses slice(-THREAD_SNIPPET_MAX_LINES), so the anchored (final) hunk line always renders; the truncation banner moved above the lines and reads "N more lines above", and the unit test asserts line 10line 39 for a 40-line hunk. Banner math (totalLineCount - lines.length) stays correct because totalLineCount is still uncapped.
  • Fixed: discussion-thread.tsx:81 gates selectThreadDiffSnippet on expanded and memoizes on primitive deps (expanded, diffHunk, subjectType, path), giving DiffLine's memo comparator a stable lines identity across reaction-toggle re-renders.
  • Accepted trade-off: numberOfLines={1} on the helper slot stays, with the author's documented rationale (a second line would move the Open button, which the change exists to prevent). The a11y half was applied: the NBSP placeholder state is now hidden from screen readers via accessible={false} + accessibilityElementsHidden / importantForAccessibility, while the invalid copy stays accessible. Not re-raised.
Notes and assumptions
  • Removing 'clipboard-empty' from PrLinkHelperMessage is safe: no remaining source, test, or E2E reference to that member or to the literal "Clipboard is empty" outside the toast call site and the pinned-copy assertion, and all three decidePrLinkPaste kinds (empty / non-url-text / valid-pr-url) are still handled in handlePaste. PR_LINK_HELPER_CLIPBOARD_EMPTY_COPY is still consumed, so check:unused should stay green.
  • Raw toast.error (rather than announcingToast.error from @/lib/a11y/announcing-toast) matches the existing pattern in this feature area (use-review-discussion-mutations.ts, use-pr-review-mutations.ts), so it is not raised as a finding — but note that the clipboard-empty outcome is now the one paste result with no screen-reader announcement, since the inline slot that carried it was removed.
  • Out of scope for this incremental diff (unchanged lines, therefore not raised inline): the Clear (X) handler at pr-review-entry-screen.tsx:248-260 does not reset helperMessage, so on any platform where TextInput.clear() does not emit onChangeText — which the component's own comment implies — the "Not a GitHub pull request link" copy can remain under an emptied field until the next real keystroke.
  • The three .kilo_workflow/learnings/*.md files are documentation only and contain no images, so the markdown-image rule does not apply.
  • No memory-leak-shaped changes in this increment: the new useMemo and the toast.error call add no subscriptions, timers, or unbounded caches.
  • Verification claims in the PR body (E2E on both platforms, screenshots) were still pending at review time; the four UX behaviours were assessed by code reading only, and no repository checks were executed in this read-only review.

Previous review (commit dd659d4)

Status: 3 Issues Found | Recommendation: Address before merge

Executive Summary

The quoted-diff snippet keeps the first 30 hunk lines instead of the last, so for long hunks it hides the very line the review comment is anchored to.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/lib/pr-review/discussion/thread-diff-snippet.ts 54 slice(0, 30) keeps the head of the hunk; GitHub's diffHunk ends at the anchored line, so the commented line is dropped for hunks over the cap

SUGGESTION

File Line Issue
apps/mobile/src/components/pr-review/discussion/discussion-thread.tsx 77 parsePatch runs for collapsed threads (contrary to the PR description) and the fresh lines objects defeat DiffLine's memo comparator
apps/mobile/src/components/pr-review/pr-review-entry-screen.tsx 290 numberOfLines={1} ellipsises the invalid-link copy at large font scales; the always-mounted NBSP slot is also announced as a blank element
Files Reviewed (21 files)
  • apps/mobile/e2e/github-api-stub/server.mjs
  • apps/mobile/src/components/pr-review/discussion/comment-row.tsx
  • apps/mobile/src/components/pr-review/discussion/discussion-thread.tsx - 1 issue
  • apps/mobile/src/components/pr-review/discussion/pr-review-discussion-list.tsx
  • apps/mobile/src/components/pr-review/discussion/reaction-picker-sheet.tsx
  • apps/mobile/src/components/pr-review/discussion/reactions-row.tsx
  • apps/mobile/src/components/pr-review/discussion/thread-diff-snippet.tsx
  • apps/mobile/src/components/pr-review/pr-review-entry-screen.tsx - 1 issue
  • apps/mobile/src/lib/pr-review/discussion/reaction-pills.test.ts
  • apps/mobile/src/lib/pr-review/discussion/reaction-pills.ts
  • apps/mobile/src/lib/pr-review/discussion/review-discussion-merge.test.ts
  • apps/mobile/src/lib/pr-review/discussion/review-discussion-reducers.test.ts
  • apps/mobile/src/lib/pr-review/discussion/review-discussion-types.test.ts
  • apps/mobile/src/lib/pr-review/discussion/thread-diff-snippet.test.ts
  • apps/mobile/src/lib/pr-review/discussion/thread-diff-snippet.ts - 1 issue
  • apps/mobile/src/lib/pr-review/pr-link-helper-slot.ts
  • apps/web/src/lib/github-pr-review/dtos.ts
  • apps/web/src/lib/github-pr-review/mappers.test.ts
  • apps/web/src/lib/github-pr-review/mappers.ts
  • apps/web/src/routers/github-pr-review-router.test.ts
  • apps/web/src/routers/github-pr-review-router.ts
Notes and assumptions
  • Backend diffHunk plumbing verified: comments(first: 50) in REVIEW_THREADS_QUERY means node.comments.nodes[0] really is the anchor comment, normalizeComment explicitly picks fields so the .strict() comment DTO is unaffected, and t.diffHunk || null maps '' to null as documented.
  • Checked for memory leaks in the new mobile code: the picker sheet's BackHandler subscription is removed on cleanup, and highlightLine's module cache is a bounded LRU. Nothing unbounded was introduced.
  • reaction-picker-sheet.tsx is a faithful copy of kilo-chat/message-reaction-picker-sheet.tsx with a distinct Portal name, and only one sheet can be mounted at a time (the component returns null while hidden), so the shared portal name is safe.
  • E2E stub changes (server.mjs) were read but held to test-harness standards; the per-platform id suffixes and dbOff offsets do not collide, and no committed spec references the new /11 identity yet. apps/mobile/e2e/AGENTS.md still documents only the #1/#2/#3 identities.
  • No E2E screenshots or verification results were attached to the PR body at review time, so the four UX behaviours were assessed by code reading only.

Fix these issues in Kilo Cloud


Reviewed by claude-opus-5 · Input: 28 · Output: 5.3K · Cached: 609.6K

Review guidance: REVIEW.md from base branch main

iscekic added 5 commits July 30, 2026 15:15
Android a11y label nodes are leaves (group card content geometrically);
helper selectors are full-string regexes (escape literals, never let an
empty findRects pass vacuously); scrollUntilVisible can land yet throw
(probe the hierarchy before retrying).
Empty-clipboard paste now surfaces 'Clipboard is empty' via sonner toast
and never sets the inline helper, so the entry layout does not engage
the message slot for that state at all. The reserved slot stays for the
invalid-link state; the helper-slot selector drops the now-unreachable
clipboard-empty state (copy constant kept for the toast). The NBSP
placeholder line is now hidden from screen readers.
GitHub's diffHunk ends at the anchored line, so the 30-line cap now
keeps the hunk's tail with a leading 'more lines above' row — the
commented line always renders. Threads no longer parse the hunk while
collapsed, and the memoized snippet keeps DiffLine's memo effective.
Android toast a11y-invisibility + deep-link dedupe techniques; iOS XCUI
a11y parents are leaves (assert by geometry); PR entry Open button's
content-driven clear-X shift is pre-existing and must be isolated from
helper-slot measurements.
Empty-clipboard paste, pasted non-URL text, and typed invalid links all
surface error toasts now; the inline helper-message UI and its reserved
slot are gone from the entry screen, so the layout has no message row at
all. The clear-button selector and the toast copy move to pr-link-paste
(pr-link-helper-slot is deleted). Open is disabled only when the field
is empty.
Comment thread apps/mobile/src/components/pr-review/pr-review-entry-screen.tsx Outdated
@iscekic iscekic changed the title feat(mobile): PR review discussion UX + no-shift entry helper feat(mobile): PR review discussion UX + toast entry feedback Jul 30, 2026
sonner toasts never enter the Android a11y tree, so the toast-only
entry feedback was silent for screen-reader users. Route the three
failure paths through announcingToast.error, which keeps the identical
visual toast and adds announceForA11y.
@iscekic iscekic added the human-ready The PR is ready for human review. label Jul 30, 2026
@iscekic
iscekic merged commit 3f85a6c into main Jul 30, 2026
22 checks passed
@iscekic
iscekic deleted the pr-review-ux-7f22 branch July 30, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready The PR is ready for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants