Android app that draws a floating bubble over the Reddit app. Tap -> extract the visible post via AccessibilityService -> Claude API summary -> overlay card. Optional second tap on the card summarizes the post's comments via Reddit's anonymous JSON endpoint.
Package: com.stanley.reddittldr. Target Reddit app: com.reddit.frontpage (also accepts any com.reddit.* prefix).
- Kotlin, minSdk 26, targetSdk 34
- Gradle Kotlin DSL with version catalog
- Compose for the in-app settings screen; classic Views for all overlay UI (
Composedoes not work well withTYPE_APPLICATION_OVERLAY) - Coroutines, OkHttp, kotlinx.serialization
EncryptedSharedPreferencesfor the Claude API keyFLAG_SECUREon the settings activity- Default Claude model:
claude-haiku-4-5-20251001
Three runtime pieces:
-
RedditWatcherService(AccessibilityService) WatchesTYPE_WINDOW_STATE_CHANGED. When Reddit comes to the foreground it startsBubbleService; when Reddit leaves it stops it. Holds auserDismissedflag that survives intra-Reddit navigation and resets only when the user genuinely leaves Reddit and returns. Owns the only public entry point for reading the post:extractCurrentPost(). -
BubbleService(foregroundService) Owns the floating bubble window, dismiss target, and summary overlay. On tap: spinner -> callextractCurrentPost()->ClaudeRepository.summarize(...)-> mountSummaryOverlay. -
PostExtractorSingle-strategy reader of the accessibility tree. No web requests, no fallbacks.
Settings UI is a regular MainActivity with Compose; the user enters their Claude API key and picks model plus summary length there.
app/src/main/java/com/stanley/reddittldr/
|-- MainActivity.kt # Settings screen host
|-- api/
| |-- ClaudeRepository.kt # summarize() + summarizeComments()
| `-- models/ # ClaudeRequest, ClaudeResponse
|-- data/
| |-- ClaudeModel.kt # Model enum (haiku/sonnet/opus IDs)
| |-- SettingsRepository.kt # EncryptedSharedPreferences-backed
| `-- SummaryLength.kt
|-- reddit/
| |-- PostContent.kt # data class - extraction result
| |-- PostExtractor.kt # single-strategy screen reader
| `-- RedditJsonClient.kt # fetchComments + searchPostId (anon)
|-- service/
| |-- BubbleService.kt # bubble lifecycle + summary orchestration
| `-- RedditWatcherService.kt # accessibility service
|-- ui/
| |-- OnboardingSection.kt
| |-- SettingsScreen.kt
| `-- overlay/
| |-- BubbleView.kt # draggable circle
| |-- DismissScrimView.kt
| |-- DismissTargetView.kt
| `-- SummaryOverlay.kt # the summary card
`-- util/
|-- DebugLog.kt # per-session in-memory log + file flush
`-- PermissionState.kt
Lives in reddit/PostExtractor.kt. The earlier three-strategy fallback chain (Compose semantic nodes / comments/{id}.json / scroll-scrape) was removed because the JSON path failed on many real posts and the Compose path was too fragile against Reddit version bumps. The current approach is one strategy: walk Reddit's accessibility windows, read visible text, scroll down, repeat.
Flow inside extract():
redditRoots()- collect everyservice.windowsroot whose package matchescom.reddit.*. Reddit's modern UI is laid out across multiple sibling windows (toolbar overlay, body container, comment sheet, and so on). Reading just one gives mostly chrome.findPostIdAcross(roots)- best-effort base36 ID lookup from view tags and hyperlinks. Used later for the comments API.readFromScreen(postId)- collect visible text top-to-bottom, scroll down withACTION_SCROLL_DOWNuntil either a comment-section marker appears (with a min-lines guard) or no new content arrives, then scroll back up. EmitPostContentwithlinesCapturedandforwardScrollspopulated for the UI footer.
ExtractionMethod is SCREEN, SCREEN_SCROLLED, or FAILED.
| Pitfall | Rule |
|---|---|
rootInActiveWindow returns the focused window. When the bubble is tapped, that is our overlay, not Reddit. |
Always go through service.windows and filter by package. Never use rootInActiveWindow. |
flagRetrieveInteractiveWindows plus canRetrieveWindowContent="true" are required for service.windows to return anything. |
These live in res/xml/accessibility_service_config.xml. Do not remove them. |
ACTION_SCROLL_FORWARD matches horizontal pagers (Reddit's swipe-between-posts container). Using it scrolls to the next post mid-extraction. |
Use AccessibilityAction.ACTION_SCROLL_DOWN.id only. Vertical-only. |
Reddit's toolbar shows a Comments button. The sticky Join the conversation / Add a comment bar is always visible. Tripping comment-boundary detection on these aborts before reading the body. |
COMMENT_MARKERS is restricted to sort by:, sort comments, be the first to comment, no comments yet, plus MIN_LINES_BEFORE_COMMENT_STOP = 8. |
looksLikeChrome substring matching ate real titles (subscription contains subscribe). |
Use a word-boundary regex and only apply it to short lines. |
| Sidebar promo subreddits got picked up by a flat regex over the whole tree. | Detect subreddit by scanning top-to-bottom visible text and taking the first r/<sub> match. |
| Lazy-list ghost nodes hang around the tree off viewport. | Filter every collected node by Rect.intersects(nodeBounds, viewport). |
Our own bubble, summary, and dismiss overlays emit TYPE_WINDOW_STATE_CHANGED. Reacting to them tore the summary down right after it appeared. |
Ignore events where event.packageName == packageName. |
Transient popups (IME, permission dialogs) made RedditWatcherService think Reddit was gone. |
Check isRedditForeground() before stopping the bubble. |
Lives in ui/overlay/SummaryOverlay.kt.
- Scrollable post-summary body
- Footer note below the body:
Captured N lines across M screens Summarize commentsbutton when the app can identify the post directly or via subreddit plus title search- Inline comments summary appended to the same scroll view
- Copy button copies post summary plus comments summary as plain text
- The card lives in a
TYPE_APPLICATION_OVERLAYwindow with a tap-outside-to-dismiss scrim
Anonymous use of https://www.reddit.com/...json. Required header:
User-Agent: RedditTLDR/1.0 (Android; by /u/stanley)
Endpoints used:
fetchComments(postId, limit)->comments/{id}.jsonsearchPostId(subreddit, title)->r/{sub}/search.json?restrict_sr=on&q=...
JBR (Android Studio's bundled JDK) is required for Gradle:
JAVA_HOME="/c/Program Files/Android/Android Studio/jbr" ./gradlew assembleDebug
cp app/build/outputs/apk/debug/app-debug.apk ~/Downloads/RedditTLDR-debug.apkThe ~/Downloads/RedditTLDR-debug.apk location is the established sideload convention.
util/DebugLog.kt keeps a per-session in-memory ring and flushes to filesDir/reddittldr_debug.log. The settings screen has a Debug logs action that surfaces it. When extraction misbehaves, the log shows fields like redditRoots=N, childCounts=..., postId=..., forwardScrolls=N, hitComments=true/false, and result=....
If you change the extractor, keep the DebugLog.logKv(...) calls in place. Without them, diagnosing extraction failures gets much harder.
comments/{id}.jsonas the primary extractor- Compose semantic-node walking by resource ID
- Sharing into the app via the share sheet
- Reading from a single Reddit window root
Use this file as the standing project memory.
Rules:
- Any meaningful code change, architecture decision, bug fix, regression, or workflow adjustment should be recorded here.
- Each note should be prefixed with the agent name in brackets, for example
[Claude]or[Codex]. - Prefer concise entries that explain what changed, why it changed, and any follow-up risk.
- Keep older entries, including failed attempts and mistakes. Do not rewrite agent log history; append corrections and follow-up outcomes instead.
- If the file's encoding gets damaged, normalize it back to plain ASCII or valid UTF-8 while preserving meaning.
Suggested note format:
### YYYY-MM-DD
- [AgentName] What changed. Why it changed. Any follow-up note.
- [Claude] Built the initial RedditTLDR Android app: accessibility-driven Reddit post extraction, floating bubble service, summary overlay, optional comment summarization via Reddit JSON, encrypted Claude API key storage, and debug logging support.
- [Claude] Documented the extractor rules and platform-specific constraints in this file so later agents would know what not to regress.
- [Codex] Fixed
BubbleServicestartup order so the app checks overlay permission before entering foreground-service mode, and stops immediately if it cannot show the bubble. - [Codex] Unified Reddit package detection in
RedditWatcherServiceby routing both event handling and foreground checks through one package-matching rule. - [Codex] Cleaned visible UI text in
strings.xml,SettingsScreen.kt, andSummaryOverlay.ktto remove mojibake and keep the interface stable. - [Codex] Established the agent-tagged memory-file convention in this project so future work can show who changed what and why.
- [Codex] Attempted to make
PostExtractorscroll more reliably by adding a fallback from explicit vertical scrolling to generic forward/backward actions on tall containers, plus a short settle retry after scrolling. - [Codex] Correction: removed the generic forward/backward fallback immediately after device testing showed it could page horizontally into adjacent Reddit posts. Kept the safer improvement: a brief post-scroll settle retry while staying strictly on explicit vertical
SCROLL_DOWN/SCROLL_UP. - [Claude] Hardened
.gitignoreto cover.kotlin/,**/build/,*.log, all keystore types (*.jks,*.keystore,*.p12,*.pem,*.key,keystore.properties),*.env, andgoogle-services.json. Original ignore only matched root-level/buildand missed the Kotlin daemon cache. - [Claude] Initialized git repo and pushed to
https://github.com/stanley-projects/RedditTLDR(private). Verified pre-push thatlocal.propertiesis not staged and no hardcoded secrets exist anywhere inapp/src. Default branchmain. - [Claude] Diagnosed why posts were always summarized off the first screen only: every test session showed
forwardScrolls=1, totalLines unchanged, hitComments=true. Hypothesis from code reading:findScrollableAcrosspicked the largest scrollable by area, which on Reddit's post screen is often the comments LazyColumn —ACTION_SCROLL_DOWNthen scrolled comments while the post body stayed put, bringing "Sort by:" into view and tripping the comment-marker boundary on the very first iteration. Codex's settle/stagnant logic could not help becausehitCommentswas set in the firstmergeOnceafter the scroll, skipping the retry path. - [Claude] Replaced
ACTION_SCROLL_DOWNwithdispatchGestureswipe inPostExtractor.kt. RemovedfindScrollableAcrossand theScrollTargetdata class. NewscrollByGesture(scrollDown)swipes vertically through the screen center for ~55% of the viewport (78% → 23%) over 350ms, awaiting completion viasuspendCancellableCoroutine+GestureResultCallback. Bypasses scrollable selection entirely; controlled distance prevents jumping past the "Sort by:" marker (which would otherwise add comment text as body). Backward restore uses the inverse swipe. Bubble lives at a screen edge, gesture is centered, so no overlay collision. Kept Codex's settle-retry and stagnant-tolerance for cases where the swipe is delivered but content hydration is delayed. - [Claude] First sideload of the gesture-based scroll hung —
Job was cancelledafter 22-58s with no[read-scroll]line ever logged. Cause:dispatchGesture(gesture, callback, null)posts the callback to the calling thread's looper. Extraction runs onDispatchers.Defaultwhich has no looper, so the callback was silently dropped andsuspendCancellableCoroutinenever resumed. Fix: addedprivate val mainHandler = Handler(Looper.getMainLooper())field onPostExtractorand pass it as the third arg todispatchGesture. Always pass an explicit looper-bound Handler when dispatching gestures from background threads.
- [Claude] Second sideload still hung the same way (
initialLines=20 hitComments=falsethen 41s of nothing). The handler fix was correct but insufficient — the actual cause was thatdispatchGesturerequires the service to declare the gesture-dispatch capability, whichaccessibility_service_config.xmldid not. Without the capability the system either silently no-ops or accepts the call without ever invoking the callback. Fix: addedandroid:canPerformGestures="true"toaccessibility_service_config.xml. Important: AccessibilityServiceInfo is cached by the system on bind — after sideloading the new APK, the user must toggle the service off and on in Settings → Accessibility for the new capability to take effect. Also added a 3-secondwithTimeoutOrNullaround the gesture await so a future failure can never hang the whole extraction again — it will return false and break the scroll loop with a timeout log entry. Added a[scroll-gesture] dispatched=true/falselog immediately after the dispatch call so we can see whether the gesture was at least accepted. - [Claude] Confirmed working after the user toggled the accessibility service in Settings to pick up the new capability. Long-post extraction now scrolls correctly via the dispatched swipe.
- [Claude] Comments-summary feature was returning bullets that didn't match the comments visible on the post. Two probable causes: (1)
findPostIdAcrossreturns<none>on every modern-Reddit screen, so we always fall back tosearchPostId, whose 60% word-overlap threshold was loose enough to match a different post with a similar title and we'd faithfully summarize the wrong post's comments — looking exactly like fabrication. (2) Comments system prompt didn't forbid invention. Fixes: raisedsearchPostIdthreshold to 75 (3-of-4 significant words). TightenedCOMMENTS_SYSTEM_PROMPTto "stick strictly to what is in the input — do not invent opinions" and "if comments are sparse or repetitive, write fewer bullets — do not pad". Threaded the fetched post's actual title back through to the summary card; footer now showsSummarized N comments from: "<title>". AddedCommentsResult(sourceTitle, sourcePermalink, comments)data class toRedditJsonClient,CommentsSummaryResult(summary, count, sourcePostTitle)toSummaryOverlay, and[redditJson] sourceTitle=…, topComment="…"debug log so the next mismatch is diagnosable from the log alone. Verification: if a wrong-post match still slips through above the 75 threshold, the user will see the wrong title in the footer and we'll know exactly which path failed. - [Claude] User reported the gesture-based extractor was always doing exactly 2 scrolls regardless of post length — too many for short posts (post fits, no scrolling needed) and too few for long posts (only 2 of N viewports captured). Cause:
MAX_STAGNANT_SCROLLS = 2was firing becauseordered.sizenot growing was being treated as "stagnant", but it can't tell the difference between "page didn't move" and "page moved but visible content was duplicates of what we'd already collected". The dispatched gesture itself was likely not moving the page at all on Reddit's Compose UI — gesture absorbed somewhere or the touched area not registering as scrollable. Fix (inPostExtractor.kt): (a) Replaced stagnant detection with a fingerprint hash of the visible-text set — if the fingerprint is identical before and after a scroll attempt, the page genuinely didn't move and we break immediately (one bail attempt instead of two). If the fingerprint changes, we keep scrolling regardless of whetherorderedgrew. (b) AddedfindBodyScrollable(roots, anchor)that picks the scrollable whose subtree contains the post heading text (or longest body line as fallback) — this is now the primary scroll mechanism, with the dispatched gesture only as fallback if no anchored scrollable is found. Resolves the "scrolling the wrong widget" issue that the largest-by-area heuristic always had on Reddit. (c) RemovedMAX_STAGNANT_SCROLLSentirely. (d) BumpedSCROLL_WAIT_MS,EXTRA_SCROLL_SETTLE_MS, andGESTURE_DURATION_MSto 500 ms each — Reddit's lazy-list hydration and slower-velocity gestures (less fling) both seem to want more time. New log fields:method=anchored-action|gesture,moved=true|false,addedLines=N,usedGestureFallback=true|false. - [Claude] First post-fix logs: anchored scroll is working (
method=anchored-action ok=true moved=trueon every iteration). All 4 test sessions exit cleanly after 1 scroll withhitComments=truebecause the post body was already captured in the initial visible-text read; the single scroll just confirmed we'd reached the "Sort by:" boundary. Body captures ranged 955-2399 chars. Same post tapped at different scroll positions yields differentbodyLen(initial read sees whatever is on screen at tap time). - [Claude] User reported "super long post but it only did 2 scrolls, missing content". Diagnosed two compounding causes from the log: (1) user's
length=SHORTsetting → "Summarize this Reddit post in 1-2 sentences" prompt, which obviously omits most details from a 2399-char body. (2) Extractor reads forward from wherever the user is on screen at tap time — if user scrolled into the post before tapping, the top portion of the body is never captured. Fix: Added a Phase-1 scroll-UP loop inreadFromScreenthat runs before the existing forward loop. Uses the samefindBodyScrollable(anchor)mechanism so it targets the post container (not the comments LazyColumn or pull-to-refresh), with the same fingerprint-based stop condition as the forward loop. Skipped when no anchor is found (gesture fallback risks triggering pull-to-refresh / back-nav) or whenhitCommentswas already true on initial read (user tapped from inside comments). Updated the restore logic to handle both directions: net displacement isforwardCount - upScrolls, restored by scrolling the inverse. Result: extraction now captures the full body regardless of where the user tapped within the post. New log fields:[read-up] moved=true|false addedLines=N scrollIndex=N,[read] upScrolls=N linesAfterUp=N. - [Claude] Above fix didn't work — user verified by placing a sentinel marker at the bottom of a long post; the summary never mentioned it. Real root cause:
ACTION_SCROLL_DOWNon Reddit's body container scrolls the entire body in one step, not one viewport. So even with the up-then-down phase structure, the down phase took one giant jump, hit the comments boundary, and exited — middle of the body never passed through the read window. The page was visibly scrolling all the way to the bottom in a single sweep, so the user could see content move past while the extractor only ever read what was on screen at the start and end. Patches over patches were never going to fix this. Rewrite ofPostExtractor.kt: torn outfindBodyScrollable, the anchored-action / gesture fallback hierarchy, theusedGestureFallbacklog field, and the multiple stagnant-detection schemes. Replaced with a single scroll primitivescrollOneStep(direction)that dispatches a controlled-distance swipe (~50% of viewport, 78% → 28%) over 500 ms, then verifies the page actually moved via fingerprint comparison. The half-viewport step size is the whole point — it gives every part of the body a chance to be in the read window between scrolls, instead of getting jumped over by a widget-defined "page".readFromScreenis now three explicit phases: initial read → walk to top → walk to bottom (stops on comment marker or no-movement) → restore.MAX_SCROLL_ITERATIONSraised to 15 to handle long posts at half a viewport per step. The architecture matches the user's mental model: incremental progress, per-step stop decision, hard safeguard when nothing changes. - [Claude] First sideload of the rewrite:
[scroll] direction=UP moved=falseanddirection=DOWN moved=falsefor both attempts — gestures dispatched but the page didn't move. Cause: the bubble overlay window has tap/drag handlers and is therefore touchable. WhendispatchGesturefires at the screen-center column and the bubble sits anywhere along that vertical line (and the user can drag it anywhere), the bubble absorbs the touch sequence and Reddit never receives it. Fix inBubbleService.kt: addedsetBubbleTouchable(touchable: Boolean)that togglesFLAG_NOT_TOUCHABLEon the bubble's WindowManager LayoutParams viaupdateViewLayout. Called withfalseimmediately afterbv.setLoading(true), restored totruein the coroutine'sfinallyblock (and infailWithToastfor early-exit paths). WithFLAG_NOT_TOUCHABLE, touches at the bubble's coordinates pass through to the next window down (Reddit), so dispatched swipes reach the post. The bubble stays visible and shows the spinner — only its touch interception is suspended for the ~1-3 seconds of extraction.
- [Claude] User scrapped the "smart stop" approach entirely and gave a deterministic spec: every bubble tap walks up to the post top (cap 5 scrolls) then down through the body and the comments (cap 6 scrolls), capturing everything in one pass; comments are then summarized from that captured pool instead of via a second Reddit-JSON round-trip; nothing persisted beyond the summary card's lifetime. Caps exist purely to bound runaway scrolling on accidental feed-page taps. Implementation: rewrote
readFromScreenas three explicit phases — Phase 1 (no-capture up scroll up toMAX_UP_SCROLLS=5), Phase 2 (read+capture, scroll down up toMAX_DOWN_SCROLLS=6, no longer exits on the comment marker), Phase 3 (split the captured visual-order list at the first comment marker into body and comments). Removed theMAX_SCROLL_ITERATIONSandMIN_LINES_BEFORE_COMMENT_STOPconstants. AddedcapturedCommentsText: StringtoPostContent.ClaudeRepository.summarizeCommentsnow takes that text blob directly (noList<Comment>plumbing); the prompt explains the input is screen-captured and tells the model to ignore interleaved chrome.BubbleService.showSummaryno longer callsRedditJsonClient.searchPostId/fetchComments— the comments callback usespost.capturedCommentsTextdirectly.RedditJsonClientis left in the codebase but no longer wired into the comments path. Net effect: predictable bounded behavior (≤11 swipes per tap), no anonymous-API dependency for comments, no possibility of summarizing the wrong post's comments. - [Claude] First sideload of the deterministic spec: scrolling worked perfectly (
6 down scrolls all moved=true,lines=122captured), butresult=FAIL bodyLen=18. The split-at-first-marker was firing too early in the captured list — almost no body survived. Cause: I deleted theMIN_LINES_BEFORE_COMMENT_STOPguard during the rewrite, but the post-screen chrome (avatar/menu/vote-bar elements) can contain text that matches our marker prefixes ("sort by:","sort comments","be the first to comment","no comments yet"), and a false-positive hit at line 1-3 truncates the body. Fix: restoredMIN_LINES_BEFORE_COMMENT_STOP = 8and applied it to the post-capture split:markerIdxis now the first index whereidx >= 8 && looksLikeCommentMarker(line). Also added failure-path diagnostics (markerIdx,markerLine,bodyLinesSnippet) and the same fields to the success path so the next mismatch is diagnosable from the log. - [Claude] Next test: scrolling and capture both fine, but the post-summary card showed the post AND comments mixed together ("...with commenters affirming it was worthwhile..."), and the "Summarize comments" button never appeared. Cause: the post had real comments, but the marker-list approach (
"sort by:", etc.) didn't match Reddit's current comment-section header text — so the split returnedmarkerIdx = -1, everything went intobody,capturedCommentsTextwas blank, and the comments button check (isBlank()) hid the button. Architecture change: stopped trying to split body from comments at extraction time. RemovedcapturedCommentsTextfromPostContent, removed the marker logic andMIN_LINES_BEFORE_COMMENT_STOPandlooksLikeCommentMarkerandCOMMENT_MARKERSfromPostExtractor. Nowbodyholds the entire captured screen content. Both summarize calls send that whole blob; the prompts do the differentiation:systemPromptForgains a leading paragraph telling the model the input includes comment-section text after the post body and to ignore it entirely, summarizing only what the OP wrote.summarizeCommentsgets a re-written prompt that tells the model to focus on the comments and explicitly say so plainly if no comments are present. Comments button visibility is now a simple length heuristic (COMMENTS_BUTTON_MIN_BODY_LEN = 500) — if we captured anything substantial, the button shows; tapping it always produces a useful response (either the discussion summary or a "no comments to summarize" message). Footer text simplified to "Comments summary" since we no longer have a precise comment count. Also droppedcountandsourcePostTitlesemantics from theCommentsSummaryResultdisplay (count was always 0 anyway with the new model). - [Claude] User confirmed end-to-end working: post summary contains only the post body (no comments bleed-through), "Summarize comments" button appears under the summary, and tapping it produces a separate comments-focused summary. The deterministic 5-up + 6-down scroll spec, prompt-based body/comment differentiation, and bubble-non-touchable-during-extraction together resolved the long-running scroll/capture/summary regressions.
- [Claude] Full SEO/AEO discoverability pass. (1) Repo metadata: added
LICENSE(MIT, Stanley Nwobosi, 2026),CITATION.cff(cff-version 1.2.0), updated GitHub topics to best 20 (addedproductivity,open-source,ai-assistant; droppedcompose,material3), updated repo description to include API-key requirement note, set homepage to Pages URL. (2) README overhaul: canonical quotable opener sentence; added latest-release shields.io badge; expanded setup with API key cost note; 8-question FAQ section targeting high-intent queries ("How do I summarize Reddit posts on Android?", "Does it need root?", etc.);<sub>keyword footer. (3) GitHub Pages site built from scratch atdocs/(served athttps://stanley-projects.github.io/RedditTLDR/):index.html(MobileApplication + FAQPage JSON-LD, full OG + Twitter Card, dark brand palette),summarize-reddit-posts.html(HowTo schema, Breadcrumb, step-by-step guide),install.html(APK download + build-from-source),faq.html(14-question FAQPage schema);favicon.svg,og-image.svg(1200×630 branded). (4) AI/LLM files in docs/:llms.txt(concise, llmstxt.org convention),llms-full.txt(extended reference for AI coding agents),robots.txt(explicit Allow for 20+ AI bots including GPTBot, ClaudeBot, PerplexityBot, Google-Extended, etc.),sitemap.xml(4 same-host github.io URLs),humans.txt,.well-known/security.txt,.nojekyll. Rootllms.txtalso updated with Pages URL and bubble-touchability note. (5) GitHub Pages enabled (POST /pages, source branch=main path=/docs). HTTP 200 confirmed immediately after push. (6) IndexNow: keyf552bcf0d7114b54a6d2b7c6e22fd40afile served at Pages, POST toapi.indexnow.org→ 202 Accepted; 4 URLs submitted. (7) Social preview PNG generated at~/Downloads/RedditTLDR-social-preview.png(198 KB, 1280×640) via Edge headless. Manual steps remaining for Stanley: (a) upload social preview PNG at github.com/stanley-projects/RedditTLDR/settings → Social preview; (b) submit sitemap at https://search.google.com/search-console.
- [Claude] Design phase started. Installed two design skills into
.claude/skills/after security audits — impeccable (Paul Bakaus, MIT/Apache-2.0, full skill with 35 reference markdowns + 16 helper scripts; only auto-allowed tool isBash(npx impeccable *)which runs the impeccable CLI by the same author) and emil-design-eng (Emil Kowalski, single 27 KB SKILL.md with no auto-tools, pure design-philosophy prose). Briefly evaluated Leonxlnx/taste-skill — security-clean but contradicted the other two on motion (perpetual loops, overshoot springs, magnetic hover, hardMOTION_INTENSITY=6/VISUAL_DENSITY=4baseline) and direction was incompatible with the restraint-first user choice; deleted from project. Createddesign-mockups/index.html(3-up comparison),combined.html(impeccable × emil), andtri-combined.html(impeccable × emil + restrained taste-skill borrowings) as visual references. User chose pure-impeccable as the direction. - [Claude] Phase 1 of the UI redesign —
SummaryOverlayand supporting drawables/colors. (See full entry below.) - [Claude] Scroll timing experiment: cut
GESTURE_DURATION_MS500→250,SCROLL_WAIT_MS500→350,EXTRA_SCROLL_SETTLE_MS500→400 in a single commit (f6b6cb3). Broke scrolling — app only scrolled twice then stopped because 350ms was shorter than Reddit's accessibility tree update time on the test device; fingerprint read "unchanged" andscrollOneStepreturned false, killing the loop. Immediately reverted withgit revert HEAD(56c6ce2). The revert restored the source to 500ms but Gradle's build cache served stale bytecode on both the firstassembleDebugand./gradlew clean assembleDebug. Required./gradlew assembleDebug --no-build-cache(or a fully fresh build environment) to force Kotlin recompilation. Lesson: after agit revert, always pass--no-build-cacheor verify compiled output explicitly — Gradle's incremental cache can serve the old bytecode even when the source is correct. App confirmed working at 500ms constants. The 500ms values are load-bearing for Reddit's tree hydration on this device and should not be tuned without device-specific profiling first. - [Claude] Phases 2 + 3 — bubble/dismiss visuals, settings screen, onboarding. Bubble (
bubble_bg.xml): replaced reddit-orange fill with dark tinted circle (#EB2A2C32) + warm-goldaccent_dotring;ic_bubble.xmlicon recolored from white to#D4B26B(warm gold) for cohesion. Dismiss target (dismiss_target_bg*.xml,DismissTargetView.kt): resting state uses hairline border on near-black; active state uses muted earthy-warm fill (#4A1E10) + accent_dot stroke instead of harsh red;OvershootInterpolator(1.6f)on entrance replaced withPathInterpolator(0.16, 1, 0.3, 1)per impeccable's "no bounce/elastic" rule. Dismiss scrim (dismiss_scrim_bg.xml): tinted dark (#19141Cfamily) instead of pure black.themes.xml: status bar and nav bar set to@android:color/transparent;windowLightStatusBar=falsekept so icons are white over dark Compose content.MainActivity.kt:darkColorScheme()replaced with a fully specifiedappColorSchememapping the impeccable palette (surface=#2A2C32, background=#19141C, primary=#F6F4F0, onPrimary=#1F2128, secondary=accent_dot, outline=hairline, error=muted terracotta#B47A6A) — all M3 components draw from our palette automatically.SettingsScreen.kt: full impeccable redesign — darkBackground(#19141C) scaffold, app-name eyebrow + "Setup" page header,SettingsSectioncomposable (tinted-neutral surface + hairline border + 22 dp radius) replaces generic M3 Card,SectionHeadercomposable (accent dot + small-caps label), all hardcodedColor.Gray/Color(0xFF4CAF50)/Color(0xFFB71C1C)replaced with palette tokens,save= weighty white-on-dark primary button,test= ghost TextButton, API-key test success usesStatusGreendot, error usesErrorTerra, OutlinedTextField colors wired to palette, FilterChips styled consistently.OnboardingSection.kt: replaces M3 Card withSettingsSection; permission rows use hairline dividers between them; granted badge is a tinted dark pill withStatusGreentext (not a harsh solid-green chip); action button usesAccentDottext color. APK at~/Downloads/RedditTLDR-debug.apk(May 3 00:50). Phase 4 queued:strings.xmlUX-writing pass (button labels, empty states, toast copy). Functionality unchanged: every existing behavior preserved (tap-outside-to-dismiss, X-icon close, partial-content banner, Copy, Summarize-comments callback, comments expansion in same scroll, captureNote line, loading state, error toast, asymmetric enter/exit timing). Visual changes only. Replaced the all-default Material card with: tinted-neutral OKLCH-derived palette (card_bg/text_primary/text_secondary/text_tertiary/text_quaternary/accent_dot/status_captured/btn_primary_*/btn_ghost_borderincolors.xml— no pure black/gray); 22 dp radius card with hairlinesurface_card_hairlineborder; serif body (Typeface.SERIF, 15.5 sp, 1.65 line-height, +0.005em tracking) for prose; system sans for UI labels; small-caps letter-spaced "SUMMARY" / "COMMENTS" eyebrows with a 6 dp warm-gold accent dot (eyebrow_dot.xml); 1 dp hairline rules between body, comments, and footer (divider_rule.xml); refined partial-content banner (small-caps with 0.5 dp warm border,partial_banner_bg.xml); weighty primary button usingbtn_primary_bg.xml(white-on-card, 0.5 dp inset highlight, 1 dp elevation); outlined ghost Copy button usingbtn_ghost_bg.xml; asymmetric padding (24 dp top, 22 dp horizontal, 16 dp bottom). Entrance is a single 340 ms fade + 0.985→1 scale + 6 dp lift viaPathInterpolator(0.16, 1, 0.3, 1)— no bounce, no perpetual motion. Exit is faster (160 ms) per emil's asymmetric-timing rule. Dropped the redundant text-only "Close" button since the X icon serves the same purpose. APK at~/Downloads/RedditTLDR-debug.apk(May 1 01:39); only Kotlin + drawables changed so no accessibility-service toggle needed. Bubble visuals, dismiss target / scrim styling, Compose settings UI, and strings copy polish are queued for follow-up turns — staged this way so any visible regression is isolated to a single surface at a time.