Skip to content

feat(tv): Continue watching on the first screen, one action row and an episode rail - #2249

Open
4gray wants to merge 4 commits into
masterfrom
claude/project-thread-igi8fx
Open

4gray wants to merge 4 commits into
masterfrom
claude/project-thread-igi8fx

Conversation

@4gray

@4gray 4gray commented Nov 14, 2026

Copy link
Copy Markdown
Owner

Requested by Sergey · project thread

What changed

Steps 2–4 of the TV webOS design review (mockups A · Home and D · Detail), built on the tokens and focus system from #2237. Layout and styles only; the remote moves the same way, except that rare watch actions now sit behind one ⋯ press.

  • Home. While there is something to continue, the hero shows the time left instead of the description (it stays in Details), and the title switcher shares the action row. The Continue watching rail now starts on the first screen (the card top is at about 640px at 1920×1080, it was about 855px).
  • Detail actions, one row: play, Start over (only while in progress), favorite, and ⋯ with Mark watched/unwatched and Reset progress. A movie in progress says where it resumes ("Resume from 42:10"). For a series, ⋯ acts on the episode that the primary button names.
  • Progress text bug (screen 33). The text floored minutes, so a position under a minute read "0 min watched" next to a filled bar. Now progressParts/progressSummary derive "42 min watched · 1 h 12 min left" from the same position and duration as the bar. Anything under a minute reads "< 1 min". Cards and the hero use the same helper.
  • Series episodes sit right under the actions, before the cast, as a horizontal rail (episode-rail.tsx):
    • One focus stop per episode, and OK plays it.
    • Each card has a fixed 16:9 frame with a progress bar or a watched ✓, so a still that loads late never changes the card size.
    • Under the rail are the selected episode's description and a single Episode options button. Down reaches the button and Up returns to the same episode. This replaces the "Watch actions" picker that sat under every card.
    • Long seasons keep only about 24 episodes in the DOM around the selected one, with spacers. The rail opens on the restored or next-to-watch episode.
  • Styles are in the new apps/tv/src/detail-actions.css. The old episode-grid rules are removed from theme.css.

Before / after

Same scenario before and after, on a private comparison page (ask for access if you can't open it): https://claude.ai/artifact/K8K8Rt6mMkJaCmJTCemcMJ. It covers screens 37, 08, 33, 10 and 11, plus two new ones: 38 (⋯ menu) and 39 (Episode options). Screen 05 (Home without history) does not change.

Not done here (deliberately)

  • The hero title is still also the first card in Continue watching. Removing it would change what the rail contains, and many Home and history flows and tests depend on that, so I'd rather do it as its own change.
  • Seasons still use the existing picker rather than chips, because tests and flows rely on it. The detail poster and the metadata pills are unchanged, and Continue watching cards keep their 2:3 shape.

Release note

  • Added .changes/tv-home-detail-episodes.md
  • Not needed

Checks

  • tv/tv-tests unit tests pass (175), including a new regression test for the progress words (4 s of 27 s: "< 1 min watched · < 1 min left", "Resume from 0:04").
  • Typecheck, lint (warnings only), release:notes:validate and tv:verify-build pass.
  • Full tv-e2e in Playwright Chromium: 229 passed, 1 skipped, 2 failed. I fixed and reran both:
    • The new episode-rail test focused a card before Details had placed its own entry focus. It now waits for that and passed 3 of 3 times.
    • history "bounds each page …" failed once on the pager and passed 3 of 3 times on rerun. It does not touch the changed code.
  • Test updates:
    • Detail Mark watched/Reset now go through ⋯, using a new moreAction helper.
    • The episode actions test now walks the rail: Down to options, Up back to the episode, and Start over on an in-progress episode.
    • episode-layout now checks that the rail stays windowed and that a late still does not move the focused episode.
    • tmdb-series checks that card height stays the same instead of the old 300/500px rows.
    • home-feature asserts that the rail is on the first screen and that the resume hero has no description.
    • The design-review scenario adds screens 38 and 39. Screen 33 now replays the 30 s sample to 8 s, because the sample ends during screens 31/32 and the old scenario saved a position near 0.
  • Docs: docs/architecture/tv-application.md (detail actions, episode rail, resume hero).
  • Not done: the Codex/Greptile local review gate (those CLIs are not available here) and a check on a real LG.

Generated by Claude Code

…n episode rail

Home: while there is something to continue, the hero shows the time left
instead of the description and the title switcher shares the action row,
so the Continue watching rail starts on the first screen.

Detail: one row of actions, play (a movie in progress says where it
resumes, e.g. "Resume from 42:10"), Start over while in progress,
favorite and a ⋯ menu with Mark watched/unwatched and Reset progress.
Watched and remaining time come from the same position and duration as
the progress bar, fixing "0 min watched" beside a filled bar.

Series: episodes follow the actions as a horizontal rail with one focus
stop per episode, a fixed 16:9 frame with progress or a watched check,
and the selected episode's description and one Episode options button
under it instead of a Watch actions picker under every card. Long
seasons keep a window of about 24 episodes in the DOM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4BLmp4uXpHf3o3owXfZ4e
@chatgpt-codex-connector

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-14T17:15:05.527982Z
We couldn’t complete this review. Comment @codex review to try again.
0bcd6a2 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@4gray 4gray self-assigned this Nov 14, 2026
@codecov

codecov Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 667 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
iptvnator-renderer 17.41MB 667 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: iptvnator-renderer

Assets Changed:

Asset Name Size Change Total Size Change (%)
chunk-1LALF9On.js (New) 2.65MB 2.65MB 100.0% 🚀
chunk-BQMNMwbV.js (New) 872.02kB 872.02kB 100.0% 🚀
chunk-CDieahjb.js (New) 350.69kB 350.69kB 100.0% 🚀
chunk-7eS6Tk8V.js (New) 325.62kB 325.62kB 100.0% 🚀
styles-O7ZH7F6J.css (New) 284.73kB 284.73kB 100.0% 🚀
main-K7VVBLGR.js (New) 276.12kB 276.12kB 100.0% 🚀
chunk-B0Qa7KHn.js (New) 222.48kB 222.48kB 100.0% 🚀
chunk-BvYjD82J.js (New) 209.44kB 209.44kB 100.0% 🚀
chunk-BnqJY6IM.js (New) 187.37kB 187.37kB 100.0% 🚀
chunk-D1A3Ft7h.js (New) 169.82kB 169.82kB 100.0% 🚀
chunk-Dlb4mNee.js (New) 166.59kB 166.59kB 100.0% 🚀
chunk-A-JZ-uIN.js (New) 159.95kB 159.95kB 100.0% 🚀
chunk-CFPgklhg.js (New) 149.78kB 149.78kB 100.0% 🚀
chunk-VSjgTtcX.js (New) 145.1kB 145.1kB 100.0% 🚀
chunk-DucPqamH.js (New) 124.14kB 124.14kB 100.0% 🚀
chunk-e6LjjlDs.js (New) 90.55kB 90.55kB 100.0% 🚀
chunk-CXEdoAK-.js (New) 70.14kB 70.14kB 100.0% 🚀
chunk-CxxHWMt5.js (New) 65.44kB 65.44kB 100.0% 🚀
chunk-BLepycvk.js (New) 59.83kB 59.83kB 100.0% 🚀
chunk-WIxNj3Gn.js (New) 55.97kB 55.97kB 100.0% 🚀
chunk-D8qaWXAI.js (New) 54.17kB 54.17kB 100.0% 🚀
chunk-BACzAli-.js (New) 52.45kB 52.45kB 100.0% 🚀
chunk-DoyIF342.js (New) 49.69kB 49.69kB 100.0% 🚀
chunk-B-JBWSdz.js (New) 46.26kB 46.26kB 100.0% 🚀
chunk-DkcYY9oZ.js (New) 43.12kB 43.12kB 100.0% 🚀
chunk-8wpNiiZ_.js (New) 40.18kB 40.18kB 100.0% 🚀
chunk-5n1qMlkR.js (New) 35.22kB 35.22kB 100.0% 🚀
chunk-A3acepSH.js (New) 34.73kB 34.73kB 100.0% 🚀
chunk-DkDW2z2p.js (New) 30.2kB 30.2kB 100.0% 🚀
chunk-D8f8iw_b.js (New) 30.11kB 30.11kB 100.0% 🚀
chunk-BlrXV8UQ.js (New) 26.8kB 26.8kB 100.0% 🚀
chunk-BpdB6myQ.js (New) 22.32kB 22.32kB 100.0% 🚀
chunk-DSA0WiVa.js (New) 21.5kB 21.5kB 100.0% 🚀
chunk-CiHSkEti.js (New) 19.91kB 19.91kB 100.0% 🚀
chunk-3BMLb9z_.js (New) 19.84kB 19.84kB 100.0% 🚀
chunk-CuL8q3B5.js (New) 18.05kB 18.05kB 100.0% 🚀
chunk-BO2JJBOi.js (New) 17.91kB 17.91kB 100.0% 🚀
chunk-CA8R19js.js (New) 16.86kB 16.86kB 100.0% 🚀
chunk-R5WTRrXB.js (New) 15.62kB 15.62kB 100.0% 🚀
chunk-DLXy-PpB.js (New) 15.4kB 15.4kB 100.0% 🚀
chunk-C41i-HM5.js (New) 13.29kB 13.29kB 100.0% 🚀
chunk-oTxOvZHs.js (New) 12.54kB 12.54kB 100.0% 🚀
chunk-DWZKeqwQ.js (New) 12.11kB 12.11kB 100.0% 🚀
chunk-J36q_cW8.js (New) 11.04kB 11.04kB 100.0% 🚀
chunk-nGW4T9IL.js (New) 10.63kB 10.63kB 100.0% 🚀
chunk-CvuNjaqk.js (New) 10.15kB 10.15kB 100.0% 🚀
chunk-DgHg7y7-.js (New) 9.49kB 9.49kB 100.0% 🚀
chunk-BtwsWzm-.js (New) 8.85kB 8.85kB 100.0% 🚀
chunk-CwSCb8nQ.js (New) 8.25kB 8.25kB 100.0% 🚀
chunk-B68ii0tj.js (New) 7.34kB 7.34kB 100.0% 🚀
chunk-DAEDjhmX.js (New) 5.67kB 5.67kB 100.0% 🚀
chunk-C8IUWt_g.js (New) 5.47kB 5.47kB 100.0% 🚀
chunk-BqN2FD8b.js (New) 5.15kB 5.15kB 100.0% 🚀
chunk-1BQ-Qu3S.js (New) 4.9kB 4.9kB 100.0% 🚀
chunk-CE0DXsv9.js (New) 4.72kB 4.72kB 100.0% 🚀
chunk-CqYngfOI.js (New) 4.45kB 4.45kB 100.0% 🚀
chunk-b--MGOa5.js (New) 4.44kB 4.44kB 100.0% 🚀
chunk-BxmgLo9H.js (New) 3.61kB 3.61kB 100.0% 🚀
chunk-LaQ_RLdz.js (New) 2.86kB 2.86kB 100.0% 🚀
chunk-BwE3lJM2.js (New) 2.83kB 2.83kB 100.0% 🚀
chunk-Czef8lZU.js (New) 2.8kB 2.8kB 100.0% 🚀
chunk-JWqLPx5U.js (New) 2.28kB 2.28kB 100.0% 🚀
chunk-63cv-qhM.js (New) 2.24kB 2.24kB 100.0% 🚀
chunk-2QAo7Gmg.js (New) 2.08kB 2.08kB 100.0% 🚀
chunk-DqPTuRcw.js (New) 2.02kB 2.02kB 100.0% 🚀
chunk-Bx5U7bKu.js (New) 1.81kB 1.81kB 100.0% 🚀
chunk-2jPJUxco.js (New) 1.8kB 1.8kB 100.0% 🚀
chunk-CZQC-ilJ.js (New) 1.73kB 1.73kB 100.0% 🚀
chunk-COTr4_aw.js (New) 1.71kB 1.71kB 100.0% 🚀
chunk-DwMFPMyw.js (New) 1.68kB 1.68kB 100.0% 🚀
chunk-IslXl1Uu.js (New) 1.68kB 1.68kB 100.0% 🚀
chunk-Dsy1Zr8-.js (New) 1.67kB 1.67kB 100.0% 🚀
chunk-BKpS7mRr.js (New) 1.67kB 1.67kB 100.0% 🚀
chunk-DzqreXAa.js (New) 1.54kB 1.54kB 100.0% 🚀
chunk-uNPfxcfv.js (New) 1.51kB 1.51kB 100.0% 🚀
chunk-mjh_Az4j.js (New) 1.51kB 1.51kB 100.0% 🚀
chunk-DK82IQpb.js (New) 1.5kB 1.5kB 100.0% 🚀
chunk-BTrEyBDg.js (New) 1.44kB 1.44kB 100.0% 🚀
chunk-CutVMZe_.js (New) 1.43kB 1.43kB 100.0% 🚀
chunk-5jYt-QDs.js (New) 1.37kB 1.37kB 100.0% 🚀
chunk-CTWrzmMo.js (New) 759 bytes 759 bytes 100.0% 🚀
chunk-CYQmyXpS.js (New) 759 bytes 759 bytes 100.0% 🚀
chunk-BSyYXiyo.js (New) 637 bytes 637 bytes 100.0% 🚀
chunk-DXXnMKsE.js (New) 588 bytes 588 bytes 100.0% 🚀
chunk-CG8jqwTj.js (New) 449 bytes 449 bytes 100.0% 🚀
chunk-BYkBch4e.js (New) 444 bytes 444 bytes 100.0% 🚀
chunk-C1ndm4SF.js (New) 443 bytes 443 bytes 100.0% 🚀
chunk-Cma13itd.js (New) 437 bytes 437 bytes 100.0% 🚀
chunk-D7T3hgi8.js (New) 437 bytes 437 bytes 100.0% 🚀
chunk-DCmNgqfv.js (New) 383 bytes 383 bytes 100.0% 🚀
chunk-1tg83C0d.js (New) 331 bytes 331 bytes 100.0% 🚀
chunk-DkbDRyS3.js (New) 284 bytes 284 bytes 100.0% 🚀
chunk-BFoPowRg.js (New) 242 bytes 242 bytes 100.0% 🚀
chunk-19hfrdbx.js (New) 239 bytes 239 bytes 100.0% 🚀
chunk-DkfJr6il.js (New) 230 bytes 230 bytes 100.0% 🚀
chunk-DpmI7UQn.js (New) 106 bytes 106 bytes 100.0% 🚀
chunk-CbqP4f3U.js (Deleted) -2.65MB 0 bytes -100.0% 🗑️
chunk-DQNbZ5Ij.js (Deleted) -872.1kB 0 bytes -100.0% 🗑️
chunk-DBGixX9j.js (Deleted) -350.69kB 0 bytes -100.0% 🗑️
chunk-qgmRCEUF.js (Deleted) -325.68kB 0 bytes -100.0% 🗑️
styles-CR3C5PBT.css (Deleted) -284.58kB 0 bytes -100.0% 🗑️
main-5HPKLNTW.js (Deleted) -276.12kB 0 bytes -100.0% 🗑️
chunk-glwGOv_P.js (Deleted) -221.9kB 0 bytes -100.0% 🗑️
chunk-Dr-LzrDi.js (Deleted) -209.44kB 0 bytes -100.0% 🗑️
chunk-DCdDv5wJ.js (Deleted) -187.3kB 0 bytes -100.0% 🗑️
chunk-Bvht38DB.js (Deleted) -169.82kB 0 bytes -100.0% 🗑️
chunk-BFKom-gc.js (Deleted) -166.59kB 0 bytes -100.0% 🗑️
chunk-DND0FVm4.js (Deleted) -159.95kB 0 bytes -100.0% 🗑️
chunk-CpqZqhag.js (Deleted) -149.78kB 0 bytes -100.0% 🗑️
chunk-B-i5zoN_.js (Deleted) -145.1kB 0 bytes -100.0% 🗑️
chunk-D-XoCCEC.js (Deleted) -124.14kB 0 bytes -100.0% 🗑️
chunk-CiRIKJ-l.js (Deleted) -90.55kB 0 bytes -100.0% 🗑️
chunk-ts_k-1z0.js (Deleted) -70.14kB 0 bytes -100.0% 🗑️
chunk-BQpp0t7i.js (Deleted) -65.44kB 0 bytes -100.0% 🗑️
chunk-5ziceBSd.js (Deleted) -59.83kB 0 bytes -100.0% 🗑️
chunk-DZDBpX4j.js (Deleted) -55.97kB 0 bytes -100.0% 🗑️
chunk-D2ivy5wj.js (Deleted) -54.17kB 0 bytes -100.0% 🗑️
chunk-eKojEG0R.js (Deleted) -52.45kB 0 bytes -100.0% 🗑️
chunk-DtgyDf_u.js (Deleted) -49.69kB 0 bytes -100.0% 🗑️
chunk-DAVxf2gv.js (Deleted) -46.26kB 0 bytes -100.0% 🗑️
chunk-LYW9PzBL.js (Deleted) -43.12kB 0 bytes -100.0% 🗑️
chunk-Q_gds0uF.js (Deleted) -40.17kB 0 bytes -100.0% 🗑️
chunk-DRnez2xd.js (Deleted) -35.22kB 0 bytes -100.0% 🗑️
chunk-CRuyqwxU.js (Deleted) -34.73kB 0 bytes -100.0% 🗑️
chunk-CC2o1xhe.js (Deleted) -30.2kB 0 bytes -100.0% 🗑️
chunk-Yd3J0JXX.js (Deleted) -30.11kB 0 bytes -100.0% 🗑️
chunk-BdlDB52U.js (Deleted) -26.8kB 0 bytes -100.0% 🗑️
chunk-0CO8cxK7.js (Deleted) -22.32kB 0 bytes -100.0% 🗑️
chunk-BKglwMBJ.js (Deleted) -21.5kB 0 bytes -100.0% 🗑️
chunk-DvZQS3Cp.js (Deleted) -19.91kB 0 bytes -100.0% 🗑️
chunk-B4Ur0-Qh.js (Deleted) -19.84kB 0 bytes -100.0% 🗑️
chunk-Bmiqms6c.js (Deleted) -18.05kB 0 bytes -100.0% 🗑️
chunk-CDOpOiXf.js (Deleted) -17.91kB 0 bytes -100.0% 🗑️
chunk-CaW7pv7f.js (Deleted) -16.86kB 0 bytes -100.0% 🗑️
chunk-aA9K46Is.js (Deleted) -15.62kB 0 bytes -100.0% 🗑️
chunk-BcuSnlpT.js (Deleted) -15.4kB 0 bytes -100.0% 🗑️
chunk-73Tpqg0d.js (Deleted) -13.29kB 0 bytes -100.0% 🗑️
chunk-CaX7ebbC.js (Deleted) -12.54kB 0 bytes -100.0% 🗑️
chunk-BQYTmkim.js (Deleted) -12.11kB 0 bytes -100.0% 🗑️
chunk--lW5kTe4.js (Deleted) -11.04kB 0 bytes -100.0% 🗑️
chunk-CoB9Tt-U.js (Deleted) -10.63kB 0 bytes -100.0% 🗑️
chunk-CchWNvnq.js (Deleted) -10.16kB 0 bytes -100.0% 🗑️
chunk-CVo4HHfT.js (Deleted) -9.49kB 0 bytes -100.0% 🗑️
chunk-CZfJ68V3.js (Deleted) -8.85kB 0 bytes -100.0% 🗑️
chunk-BHwVim9B.js (Deleted) -8.25kB 0 bytes -100.0% 🗑️
chunk-WCOoULeo.js (Deleted) -7.34kB 0 bytes -100.0% 🗑️
chunk-CyFhJrkD.js (Deleted) -5.67kB 0 bytes -100.0% 🗑️
chunk-lnn8OeNO.js (Deleted) -5.47kB 0 bytes -100.0% 🗑️
chunk-BFlF0Moq.js (Deleted) -5.15kB 0 bytes -100.0% 🗑️
chunk-D0t0Foie.js (Deleted) -4.9kB 0 bytes -100.0% 🗑️
chunk-BGYvhmJU.js (Deleted) -4.72kB 0 bytes -100.0% 🗑️
chunk-frDFPg5s.js (Deleted) -4.45kB 0 bytes -100.0% 🗑️
chunk-s8TOvq6_.js (Deleted) -4.44kB 0 bytes -100.0% 🗑️
chunk-C2gPqArV.js (Deleted) -3.61kB 0 bytes -100.0% 🗑️
chunk-CSRfRjXV.js (Deleted) -2.86kB 0 bytes -100.0% 🗑️
chunk-D3yQ9CGJ.js (Deleted) -2.83kB 0 bytes -100.0% 🗑️
chunk-C9YIlXeD.js (Deleted) -2.8kB 0 bytes -100.0% 🗑️
chunk-2uBv1yqu.js (Deleted) -2.28kB 0 bytes -100.0% 🗑️
chunk-BiQwU_5o.js (Deleted) -2.24kB 0 bytes -100.0% 🗑️
chunk-Dmaf1Yqf.js (Deleted) -2.08kB 0 bytes -100.0% 🗑️
chunk-ymn7ZZhJ.js (Deleted) -2.02kB 0 bytes -100.0% 🗑️
chunk-DDbCPVx7.js (Deleted) -1.81kB 0 bytes -100.0% 🗑️
chunk-DuB6QQZ6.js (Deleted) -1.8kB 0 bytes -100.0% 🗑️
chunk-C4ZMmDlk.js (Deleted) -1.73kB 0 bytes -100.0% 🗑️
chunk-GSjqdrXm.js (Deleted) -1.71kB 0 bytes -100.0% 🗑️
chunk-YGQmAxD6.js (Deleted) -1.68kB 0 bytes -100.0% 🗑️
chunk-Cs71969y.js (Deleted) -1.68kB 0 bytes -100.0% 🗑️
chunk-WieUUuP8.js (Deleted) -1.67kB 0 bytes -100.0% 🗑️
chunk-CYAdVj2V.js (Deleted) -1.67kB 0 bytes -100.0% 🗑️
chunk-DHMzjfDj.js (Deleted) -1.54kB 0 bytes -100.0% 🗑️
chunk-sA2N33-6.js (Deleted) -1.51kB 0 bytes -100.0% 🗑️
chunk-aMCLkTTB.js (Deleted) -1.51kB 0 bytes -100.0% 🗑️
chunk-BQSCX6e9.js (Deleted) -1.5kB 0 bytes -100.0% 🗑️
chunk-C1ck-5LO.js (Deleted) -1.44kB 0 bytes -100.0% 🗑️
chunk-CCUrhQTb.js (Deleted) -1.43kB 0 bytes -100.0% 🗑️
chunk-rEeyH3GR.js (Deleted) -1.37kB 0 bytes -100.0% 🗑️
chunk-BYjoy8YU.js (Deleted) -759 bytes 0 bytes -100.0% 🗑️
chunk-TiCdNCXM.js (Deleted) -759 bytes 0 bytes -100.0% 🗑️
chunk-B6g2ZdNR.js (Deleted) -637 bytes 0 bytes -100.0% 🗑️
chunk-Ckws6xtL.js (Deleted) -588 bytes 0 bytes -100.0% 🗑️
chunk-BafOST82.js (Deleted) -449 bytes 0 bytes -100.0% 🗑️
chunk-CZEfoyzh.js (Deleted) -444 bytes 0 bytes -100.0% 🗑️
chunk-CvlxP0A-.js (Deleted) -443 bytes 0 bytes -100.0% 🗑️
chunk-By4U4Ota.js (Deleted) -437 bytes 0 bytes -100.0% 🗑️
chunk-D5xzkEwu.js (Deleted) -437 bytes 0 bytes -100.0% 🗑️
chunk-Ca0VagtT.js (Deleted) -383 bytes 0 bytes -100.0% 🗑️
chunk-D0nzRWVh.js (Deleted) -331 bytes 0 bytes -100.0% 🗑️
chunk-DfsBPSIU.js (Deleted) -284 bytes 0 bytes -100.0% 🗑️
chunk-czhf8oba.js (Deleted) -242 bytes 0 bytes -100.0% 🗑️
chunk-CraSGbOI.js (Deleted) -239 bytes 0 bytes -100.0% 🗑️
chunk-i4S1UMeQ.js (Deleted) -230 bytes 0 bytes -100.0% 🗑️
chunk-BwWmp409.js (Deleted) -106 bytes 0 bytes -100.0% 🗑️

@greptile-apps

greptile-apps Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge, with a non-blocking focus issue when scrolling left through a long season.

Findings

  1. P2 Scrolling drops episode focus ▶
Fix with agent prompt
### Issue 1
libs/tv/feature/src/episode-rail.tsx:139-149
Scrolling left of a focused later episode can lose focus. `ranges.map` puts each range in a separate nested array. When scrolling splits one range into two, the selected episode moves to the second array. Preact replaces its button despite `key={e.id}`, because the key now belongs to a different parent. The next remote arrow starts elsewhere on the page.

Flatten the ranges into one keyed child list so changing the visible window preserves each episode's button.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR brings Continue watching higher on Home, groups rare detail actions into menus, and replaces the episode grid with a horizontal rail.

  • The Home resume hero makes room for Continue watching on the first screen.
  • Series detail pages show episodes in a focused horizontal rail.
  • Movie and series details keep less-used watch actions behind one menu.
  • Viewing progress labels match the bar and show the resume time.

Reviews (3) · Last reviewed commit: "fix(tv): render scrolled-to episodes and..." · Reviewed by Greptile

Comment thread libs/tv/feature/src/episode-rail.tsx Resolved
Comment thread apps/tv-e2e/src/design-review.e2e.ts Resolved
Comment thread libs/tv/feature/src/episode-rail.tsx Resolved
The episode rail scrolled to its starting episode once, using the initial
480px step guess. Below 1400px the measured step is smaller, the leading
spacer shrank and a long season could open on an empty rail. Repeat the
placement when the step changes, until it is measured or the viewer moves.

The design-review capture now restarts the sample with Watch again when it
finished during the player screens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4BLmp4uXpHf3o3owXfZ4e

4gray commented Nov 14, 2026

Copy link
Copy Markdown
Owner Author

Both Greptile findings were real. They are fixed in 4ba21a3:

  1. Later episodes open off-screen. The rail used to place its starting episode once, using the 480px step guess. It now repeats that placement when the measured step changes, until the step is measured or the viewer moves within the rail. At 1920 the guess already matched (448 + 32). Below 1400 the step is smaller, so the spacer shrank after the first scroll.
  2. Finished playback breaks captures. The design-review scenario now uses Start over when it exists and falls back to the play button (Watch again) when the sample has finished.

Rechecked: episode-layout, vod-comfort, tmdb-series, tmdb-person, comfort-integration and presentation (39 passed), plus the design-review capture.


Generated by Claude Code

Pointer or wheel scrolling now moves a second, small window of rendered
episodes, while the selected episode stays mounted. The rail test scrolls
to the end of a 200-episode season and checks the last episode renders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4BLmp4uXpHf3o3owXfZ4e

4gray commented Nov 14, 2026

Copy link
Copy Markdown
Owner Author

22faa5a addresses the remaining items:

  • Typecheck Jest spec programs: the rail position helper in episode-layout.e2e.ts now casts to HTMLElement.
  • Scrolling reveals empty episode slots (Greptile): scrolling the rail with a pointer or wheel now renders a small second window around the scrolled-to position. The selected episode stays mounted, and overlapping windows are merged. The rail test scrolls a 200-episode season to the end and checks that the last episode renders while focus stays put.

Ran locally: the episode-layout, vod-comfort, tmdb-series, comfort-integration and design-review e2e suites (22 passed, 1 skipped), the app and spec typechecks, and lint.


Generated by Claude Code

@codecov

codecov Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.88%. Comparing base (ba38f21) to head (0c279be).
⚠️ Report is 3 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff            @@
##           master    #2249    +/-   ##
========================================
  Coverage   81.88%   81.88%            
========================================
  Files        1904     1905     +1     
  Lines      101617   101633    +16     
  Branches    24089    24852   +763     
========================================
+ Hits        83204    83218    +14     
  Misses      11128    11128            
- Partials     7285     7287     +2     
Flag Coverage Δ
unit 81.88% <ø> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@4gray

4gray commented Nov 14, 2026

Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread libs/tv/feature/src/episode-rail.tsx Outdated
Comment on lines +139 to +149
{ranges.map(([from, to], part) => [
from > (part ? ranges[part - 1][1] : 0) ? (
<span
key={`spacer-${from}`}
class="episode-spacer"
aria-hidden="true"
style={{
width: `${(from - (part ? ranges[part - 1][1] : 0)) * step}px`,
}}
/>
) : null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Scrolling drops episode focus

Scrolling left of a focused later episode can lose focus. ranges.map puts each range in a separate nested array. When scrolling splits one range into two, the selected episode moves to the second array. Preact replaces its button despite key={e.id}, because the key now belongs to a different parent. The next remote arrow starts elsewhere on the page.

Flatten the ranges into one keyed child list so changing the visible window preserves each episode's button.

Prompt To Fix With AI
This is a comment left during a code review.
Path: libs/tv/feature/src/episode-rail.tsx
Line: 139-149

Comment:
**Scrolling drops episode focus**

Scrolling left of a focused later episode can lose focus. `ranges.map` puts each range in a separate nested array. When scrolling splits one range into two, the selected episode moves to the second array. Preact replaces its button despite `key={e.id}`, because the key now belongs to a different parent. The next remote arrow starts elsewhere on the page.

Flatten the ranges into one keyed child list so changing the visible window preserves each episode's button.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Render the episode window as one flat keyed list, so scrolling before a
focused later episode no longer remounts it and drops focus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4BLmp4uXpHf3o3owXfZ4e

4gray commented Nov 14, 2026

Copy link
Copy Markdown
Owner Author

0c279be fixes Greptile's "Scrolling drops episode focus" finding. The rail window is now rendered as one flat keyed list, so an episode keeps its button when the window splits or merges around it. The rail test now also scrolls back to the start, past a focused later episode (31 and 121 of 200), and checks that focus stays on that episode.

The episode-layout, vod-comfort, tmdb-series and comfort-integration e2e suites pass (22 passed), along with the typecheck and lint.


Generated by Claude Code

4gray commented Nov 14, 2026

Copy link
Copy Markdown
Owner Author

On 0c279be, "Performance journeys" fails on one counter: launch/renderer.cdTicksIdle30s is 4 ticks against a baseline of 3. I don't think this PR causes it:

  • The counter measures Angular change detection in the Electron renderer. This PR changes only the Preact TV app (libs/tv/*, apps/tv).
  • The same check passed on the previous head, 22faa5a. Since then, the only change is the episode-rail render in libs/tv/feature.
  • The other 15 baselines pass in this run, and one of them, launch/renderer.domMutationsToFirstCard, is 33 below its baseline.

It looks like noise of one idle tick. A re-run of the failed job should clear it. I'm not changing any baselines.


Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants