Skip to content

Release v3.27.0 → v3.38.38: pin system + V8 OOM fix + lazy load + perf#14

Merged
jouki merged 121 commits intomasterfrom
dev
Apr 15, 2026
Merged

Release v3.27.0 → v3.38.38: pin system + V8 OOM fix + lazy load + perf#14
jouki merged 121 commits intomasterfrom
dev

Conversation

@jouki
Copy link
Copy Markdown
Owner

@jouki jouki commented Apr 15, 2026

Summary

Big release — 121 commitů od posledního master deploye (v3.26.4). Hlavní oblasti:

🛡️ Stability

  • V8 OOM crash fix (per-channel LRU dedup, ~1.5 MB cap místo unbounded) — Chrome renderer crash při dlouhých session na busy streamech
  • Boot watchdog + auto-dump — auto-stáhne log když init trvá > 20s (přežije i frozen sidepanel)
  • Cache hydration 77s → ~2s — chunked insert přes requestIdleCallback, lazy-load 250 zpráv při bootu, scroll-up načte další 150 batches
  • Scroll lock při paused — scrollTop preservation aby nové zprávy neposouvaly reading line

📌 Pin systém (v3.38.0–v3.38.38)

  • Per-channel LRU dedup (Map<"platform:channel">, 150 channelů × 250 IDs)
  • GQL GetPinnedChat fallback — pin se zobrazí i když je vanilla chat hidden
  • DOM + GQL per-field merge (_lastGoodPinCache drží gold standard data)
  • Idempotent re-render (hash check) + collapsed state preservation
  • Fancy amber design s pulse animací, readable author colors, badges + emotes
  • Unified pin path (legacy #pinned-banner#highlights-banner)
  • /uc pin [text] mock command (stackuje s real pinem)

🎨 Twitch events + UI

  • Sub / resub / gift / sub bundle / redeem render with bilateral borders + tier styling (Prime cyan, Tier 2 cyan, Tier 3 gold)
  • Cheermotes (Cheer{N} → tier emote + colored bit count)
  • Twitch credits mirror (bits + channel-points + claim bonus pill)
  • Community highlights mirror (raid, hype train, gift leaderboard, pin) into UC banner
  • Mod actions (CLEARCHAT/CLEARMSG): timeout / ban / delete pill + greyed line-through
  • Color resolution: DOM scrape → GQL fallback → readableColor() WCAG boost
  • 7TV paints na nicknames (LINEAR/RADIAL/URL paints)
  • Per-user 7TV emote loadouts (cross-channel personal emotes)
  • @mention coloring (incl. bare-name mentions)
  • Schemeless URL linkification

🖱️ UX

  • Lazy scroll-up load (max 5000 v cache, 250 v DOMu, batch 150 při scroll up)
  • Spinner při hydrate older messages, no false "N nových zpráv" pill
  • CS plural rules (1 / 2-4 / 5+)
  • Hide-not-collapse Twitch chat (rewards popover stays visible přes portal)
  • Top-nav restore button (vždy injected)
  • Always-on UC chat-toggle button

⚡ Perf

  • Proactive DOM highlight scan (SCAN_HIGHLIGHTS_NOW) — 10-30ms místo waiting na MutationObserver
  • Watch-streak filter (regex check) — keep-last credits keep visible during +10 animation
  • 5min color revalidation
  • Boot instrumentation (_bootMark per fáze + heap size)

🐛 Misc

  • Backup utility (extension/backup.html) — export/import storage
  • Per-channel + per-emote 7TV paint lookup
  • Optimistic message badges from cached entries
  • Reply emote position offset fix
  • Channel-points icon reset on auto-switch
  • Top-nav speech-bubble restore button

Test plan

  • Boot na busy streamu (Cohh, > 1000 viewers): heap roste lineárně, ne OOM
  • Cache load < 3s (default 5000 msgs storage, 250 v DOMu)
  • Scroll up načte další 150 zpráv s spinnerem, scrollPos preserved
  • Pin via UC button → jedna fancy karta v highlights banneru, žádná duplicita
  • Pin z chatu cizím modem → DOM scrape doplní emoty + badges
  • /uc pin Test → mock card stackuje s reálným
  • Channel switch resetuje pin cache + dedup
  • Sub/gift/redeem renderují s tier styling
  • Cheermotes z body parsovány
  • Credits pill s claim bonus funkční

🤖 Generated with Claude Code

jouki and others added 30 commits April 15, 2026 10:14
…om tooltip

IVR badge API returns title field (e.g. "Moderator", "Broadcaster", "Verified",
sub tenure strings) — store {url, title} in _twitchBadges instead of raw URL,
render as data-tooltip so platform-badge custom tooltip picks them up. Extend
selector from .pi[data-tooltip] to also match .bdg-img[data-tooltip].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…edeem events

USERNOTICE msg-ids: sub, resub, subgift, submysterygift — each emits a
structured message with isSubEvent/isSubGift/isGiftBundle flag. Dedicated
renderers (_renderSubEvent, _renderGiftEvent, _renderRedeemEvent) produce
platform-less layouts: purple pill for gift bundles with ×N counter,
per-gift lines with gift icon + "Gifted a Tier N Sub to Recipient", resub
lines with months + streak, redeem lines with reward name placeholder + cost.

PRIVMSG with custom-reward-id → isRedeem (reward name N/A anonymously, only
UUID exposed); msg-id=highlighted-message → isHighlight accent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…k bg

Twitch's vanilla chat silently lifts dark user colors (e.g. DarkRed #8B0000)
so they read on dark backgrounds — otherwise "named" CSS colors like DarkRed
render nearly invisible. readableColor() clamps HSL L to [0.5, 0.88]
(Memoized). Applied at every .un style.color assignment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e GQL

_flushColorLookups now queries open Twitch tabs for rendered .seventv-chat-user
(or .chat-author__display-name) inline colors first — free, matches exactly
what vanilla chat shows (user-set hex included). Only usernames still missing
after the DOM phase fall through to the GQL batch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
IRC carries only redeems that require a message body — community-goal
contributions, "unlock an emote", and other text-less PubSub events never
reach an anonymous justinfan connection. content/twitch.js now runs a
MutationObserver over the live Twitch chat DOM and relays any line whose
text or className matches a redeem/highlight/community-goal heuristic
(username + "redeemed " + reward + optional cost).

Sidepanel._handleDomRedeem dedups (5s bucket + username+reward+msg key)
and funnels into the existing isRedeem render path. Channel-scoped so
redeems from other open Twitch tabs don't leak in.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…OM alive

- Reply bodies strip "@username " prefix; emote tag positions are still
  computed from the ORIGINAL message. Previously we nulled the emotes tag
  entirely, so subscriber/native Twitch emotes like always43Booty inside
  replies rendered as plain text. Now we track the stripped prefix length
  (twitchEmotesOffset) and shift positions in _splitTwitchEmotes.
- UC button no longer collapses vanilla Twitch chat (which unmounts
  .chat-shell and breaks DOM mirroring for redeems, credits, colors).
  Instead injects a scoped <style> that hides the right-column via
  width:0 + visibility:hidden. Reversed when UC closes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Optimistic messages seed badgesRaw from a last-known cache entry that can be
stale (previous channel, pre-sub session, etc). _upgradeOptimistic previously
only added badges when the DOM had none, so stale partial sets (just the sub
badge) would stick even when the real IRC echo carried the full set (mod +
sub + founder…). Now we always remove and re-render badges on upgrade.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… chat

Twitch reserves horizontal space for the chat column via BEM modifier
classes on .channel-root__player and .channel-root__info (--with-chat
suffix). Hiding the right-column alone left that gap in place. Now when
UC hides chat we remove the modifier (marking elements with a data-attr
for later restore) and run a MutationObserver to re-strip on SPA/channel
re-renders. Closing UC restores the modifier.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e Sbalit

- content/twitch.js: capture-phase click on [data-a-target=
  right-column__toggle-collapse-btn] short-circuits Twitch's collapse
  (which unmounts .chat-shell and kills DOM mirroring) and toggles our
  hideTwitchChatVisually soft-hide instead. Chat stays live.
- Community highlights MutationObserver snapshots .community-highlight
  cards (hype train, gift-sub leaderboard, pinned message) — classifies
  kind heuristically, relays text to sidepanel via TW_HIGHLIGHTS. Diff
  hashing collapses spurious re-emits.
- Sidepanel renders a compact #highlights-banner above #chat with a
  per-card pill (locomotive icon for hype train, gift for leaderboard).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…isting IRC entry

Previously IRC (custom-reward-id PRIVMSG → "Channel Points Reward"
placeholder + user message) and the DOM mirror (real reward name + cost,
no message) both emitted separate messages for the same redeem — and the
DOM extractor sometimes scraped a multi-chat-line container, concatenating
"reward2000mojma98: Veru Veru..." into the reward field.

Fixes:
- extractRedeem rejects container nodes with >1 chat-line descendant,
  inline message bodies, or a ":" after "redeemed" in the snapshot text.
- _handleDomRedeem looks for a recent (<10s) IRC redeem from the same
  user and upgrades its reward name/cost in place instead of adding a
  duplicate message.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_processMentions only consulted _chatUsers, which stays empty for users
who were merely @mentioned without having sent a message. Those mentions
got no color at all (fell back to default), while mentions of the same
user AFTER they spoke looked correct.

- Tag every .mention span with data-mention-user so retints find them.
- For Twitch mentions missing a _chatUsers entry, enqueue
  _enqueueTwitchColorLookup — same pipeline (DOM scrape → GQL batch) we
  already use for chat-line usernames.
- _flushColorLookups retints .mention[data-mention-user=…] spans in both
  the DOM-resolved and GQL-resolved branches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When UC hides the vanilla chat we inject a speech-bubble icon into
.top-nav__actions (just left of the user avatar/bell). Clicking it
reverses hideTwitchChatVisually → chat reappears and the button removes
itself. MutationObserver re-injects on SPA navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous .top-nav__actions selectors didn't match Twitch's current DOM —
button never appeared. Now: try a few semantic data-a-target selectors,
then walk up from the user avatar / login button to the first short flex
row holding multiple siblings. Insert the button immediately left of the
avatar wrapper instead of at row start.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs in _scrapeExistingChat could silently drop messages from the
gap between cache and live IRC:

1. 120s cache-recent skip: any cached message younger than 2 minutes
   would short-circuit the entire scrape, so messages that arrived
   while UC was closed for 30–120s never made it in. Removed — dedup
   downstream (_seenContentKeys / _seenMsgIds) handles overlap.

2. Boundary detection picked the LATEST suffix match in scraped
   usernames, so when the same user-pair repeats later in chat we'd
   align the cache to that later position and skip the messages
   between the two occurrences. Now iterates start→end and picks the
   earliest match — possible dupes get deduplicated by content key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clicking the 💾 button now bundles a snapshot of common debugging state
into the log so a single download answers most "why is X colored Y"
questions without follow-up:

- version, channel/config, provider connection state
- cache + dedup-set sizes, badge/paint definition counts
- per-rendered-Twitch-user color/paint/userId/_fromGQL/_paintChecked
- chrome.storage.local.uc_user_colors slim summary
- live Twitch DOM color snapshot per open Twitch tab (ground truth)
- last 30 cached messages with relevant flags

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three reasons the dump was practically empty:

1. MV3 service worker sleeps after ~30s idle and loses module-scope
   _logs[] on wake. Now persisted to chrome.storage.session and
   hydrated before any read/write so logs survive worker restarts.
2. UC_LOG handler expected msg.args but the new diagnostics dispatch
   sent msg.text — payload was silently dropped. Handler now accepts
   either field and the relay returns ack.
3. Sidepanel triggered DUMP_LOGS without awaiting the UC_LOG
   round-trip, so the file write often raced ahead of the diag append.
   Now awaits.

Empty-log dump now writes a one-line note explaining the worker just
woke up so users know to reproduce + redump.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… + WCAG-aware readability boost

Diagnosed from a debug-log dump: zakk_lh's entry kept showing _fromGQL:false
and entryColor:"#0000FF" no matter how many lookups fired, because every
new IRC PRIVMSG from that user was overwriting the _chatUsers entry with
a fresh {name, platform, color, badgesRaw} object — wiping _fromGQL,
_paint, _paintChecked, userId. Lookup queue refired forever; the rendered
color stayed stuck on raw IRC #0000FF (boosted only to #8B58FF instead of
Twitch's #9999FF).

- Preserve prior entry fields when overwriting on each IRC message.
- Don't downgrade a GQL/DOM-resolved color back to raw IRC color (the
  resolved value already includes Twitch's readability adjustment).
- readableColor: use WCAG relative luminance to set the L floor — pure
  blue (luminance ~0.07) now lifts to L=0.78 to match Twitch (~#9999FF),
  while pure red stays unboosted (luminance ~0.21).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Twitch's redeem line textContent concatenates "<reward>" + "<cost>" with
no separator, so "Contribute to Cult's Totem500" arrived as a single
string. The previous regex first failed to split on the icon then the
fallback grabbed trailing digits as cost — but didn't strip them from
the rewardName. We rendered the reward as "Contribute to Cult's
Totem500" alongside the correct ⊙ 500 cost pill.

Now: extract everything after "redeemed", then peel the trailing digit
run (with optional preceding bullet/icon) off as the cost.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_processMentions previously only handled @username mentions. Twitch's
vanilla chat also colors bare usernames inside message bodies (StreamElements
"sullivan_cz odchází…", quoted handles, etc.).

Pass 2 walks remaining text nodes (skipping anything already inside a
.mention span), tokenizes on word boundaries (3–25 alphanumeric/underscore),
and wraps any token that exists in _chatUsers as a chatter. Restricting
to known chatters keeps generic words from getting falsely colored.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
snapshotHighlights queried four overlapping selectors and the same DOM
node matched several of them — wrapping container + inner card got
emitted as separate cards, so "Pořiďte si předplatné a získejte Drops!"
showed three times in our top banner.

Now: collect all candidates first, drop any element that's an ancestor
of another candidate (keep innermost matches), then text-level dedup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The original Twitch global face emotes ship at a much lower native
resolution than channel/subscriber emotes. Scaling them up to our
standard chat-emote size made them blurry and pushed them visually
out of proportion with vanilla Twitch chat.

Detected via the small-integer ID (≤100) inside the jtvnw v2 URL —
this only matches Twitch's emoticon CDN so BTTV/FFZ/7TV emotes that
happen to share a name (e.g. someone's BTTV ":D") are unaffected
(they live on different CDNs). Tagged with .emote-tiny class; CSS
shrinks height to 1.25em (1.4em medium / 1.55em large).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous detection used a small-integer ID regex on the jtvnw v2 URL.
That misses face emotes Twitch has reassigned to large IDs in their
modern emote system — most notably <3 (now 555555584) and several
others.

Switched to a name-based set (':)', ':D', ':O', ';)', 'B)', '<3', 'O_o',
…) gated on a Twitch CDN URL — so BTTV/FFZ/7TV emotes that share the
same name still render at full size (they live on different domains).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
We rendered emotes at ~28–32px CSS but were fetching the smallest CDN
variant (Twitch /1.0 = 28px, BTTV /1x ≈ 28px, FFZ urls['1'], 7TV
1x.webp). On any hi-DPI display the browser had to upscale, so emotes
came out blurry vs vanilla Twitch (which requests larger natives).

Switched all providers to prefer the 2x variant (Twitch /2.0, BTTV
/2x, FFZ urls['2'], 7TV 2x.webp) with graceful fallback to 1x where
2x isn't published. Browser now downscales — sharp result. .emote-tiny
face emotes still shrink to ~20px CSS so quality remains crisp even
when /2.0 of an OG face is a server upscale.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diagnosed from the log: roschei had _chatUsers entry {color:"#008000",
_fromGQL:true} despite Twitch DOM ground truth being rgb(0,179,0). Same
user could appear in two shades depending on lookup timing — the IRC
echo path was overwriting prev resolved color whenever msg.color was set.

Two paths needed the fix (only one was patched in 3.28.8):
- The "track color + badges before dedup" block (~5103) used a
  keepGqlColor heuristic gated on msg._needsColorLookup. When IRC sent
  an explicit color tag, msg._needsColorLookup was false → resolved
  color got clobbered. Now: if prev._fromGQL, always keep prev.color.
- _upgradeOptimistic also overwrote with a bare 4-field object losing
  prev resolved state. Now spreads prev and uses same preserve rule.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hover action cluster (copy + reply + pin) sat on top of the message
text — covering inline emotes the user wanted to hover for the preview
card. Now smaller (20×18 buttons, 11px svg) and offset above the
message (top: -14px) so emotes stay reachable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The browser-native title= tooltip popped up on every emote hover and
visually competed with our own emote-preview card (which already shows
the name + source label). Dropped — alt= remains for accessibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. GQL fallback set _fromGQL=true even when GQL returned no color and we
   were keeping the raw IRC hex from prev. _enqueueTwitchColorLookup
   then skipped that entry forever, leaving roschei et al. stuck on
   #008000 even though Twitch DOM had rgb(0,179,0). Now: only mark
   _fromGQL when GQL actually produced a color.
2. Existing chrome.storage.local.uc_user_colors entries from earlier
   buggy builds carry the bad combo (raw hex + _fromGQL true). On
   hydrate, sanitize: drop _fromGQL when color is still a #rrggbb so
   the lookup queue re-attempts on the next message.
3. Emote heights bumped (1.75em → 2em small, 2em → 2.25em medium,
   2.2em → 2.5em large) — vanilla Twitch renders ~28px chat emotes
   and our previous height visibly read smaller side-by-side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Raid notifications used a thin red border-left + small right-side pill,
easy to miss in fast chat. Now styled like Announcements: bilateral
gradient border (orange→red→yellow), glow, and a header bar with
pulsing rocket icon, RAID label and viewer count pill.

/uc mock commands rewritten to cover everything we render: raid (with
viewer count), announcement [PRIMARY|BLUE|GREEN|ORANGE|PURPLE], sub,
resub (with months + streak), subgift, giftbundle [N], redeem [name]
[cost], highlight, timeout [s], ban, delete. raider/first/sus kept.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Twitch sends msg-param-sub-plan="Prime" for free Prime subs; we mapped
it to "Tier 1" via the lookup default. Now: when subPlan is Prime, the
sub event gets a crown SVG (instead of star), the body reads
"Subscribed with Prime." (gradient cyan/blue), and the row uses Twitch
Prime's signature blue accent + glow.

/uc prime mock added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ier-tinted accents

Tier 1 sub previously rendered as a thin purple left-border + light bg;
Prime got the prominent gradient/glow treatment in 3.30.1 so paid subs
looked underwhelming next to it. Now all sub events use the bilateral
border + glow + tinted icon callout style — Tier 1 stays purple, Tier
2 picks up cyan, Tier 3 picks up gold, Prime stays Twitch-Prime blue.

/uc sub2 and /uc sub3 mocks added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jouki and others added 29 commits April 15, 2026 22:51
v3.38.8 diagnostic Pin dump odhalil skutečnou Twitch pin footer
strukturu:

  <p>
    <span>[3 badge imgs]</span>
    <span class="chatter-name">
      <span class="CoreText">
        <span style="color: rgb(218, 165, 32);">Meewile</span>
      </span>
    </span>
    odesláno v 08:34 PM
  </p>

Author je UVNITŘ time container, ne vedle. v3.38.9 secondary
heuristic hledal timeEl v span/a only + walked previous siblings.
Ale timeEl je <p>, sibs.indexOf(<p>) v spans/a = -1 → loop
přeskočen → authorEl zůstal null.

Fix:
1. Accept <p>/<div>/<span>/<a> jako timeEl kandidáta.
2. Primary: search INSIDE timeEl pro styled short-text span,
   prefer leaf-first (innermost colored span = exact author).
3. Fallback: walk previous siblings (pro layouts kde author
   je next to time, ne uvnitř).

Badges path z v3.38.9 by pak fungoval — hop up z authorEl
dokud nenajde flex/grid row a scan badge imgs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
….11)

User report: pin banner se neukáže když vanilla Twitch chat je
collapsed (ikdyž v UC panel by user chtěl vidět). Root cause:
Twitch v collapsed mode nerenderuje .community-highlight-stack__card
full cards, místo toho compact "!N" badge na video playeru. Content
script HIGHLIGHT_SELECTORS nenašly nic.

Změny:
1. Přidané selektory: [class*="community-highlights"],
   [data-a-target="community-highlights"], .pinned-chat__highlight-card.
2. Rate-limited diagnostic (1× / 30s) — když snapshotHighlights
   nenajde nic ale body obsahuje pin/highlight-related elementy,
   dumpne outerHTML prvních 6 kandidátů jako HighlightDiag log.
   Další iterace použije skutečnou collapsed-mode DOM strukturu.

Follow-up plán: až budeme mít HighlightDiag dump, extractor se
adaptuje. Long-term možnost: GQL fallback přes CHECK_PIN handler
co už existuje (channel.pinnedChatMessages query), funguje bez
ohledu na chat collapsed state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…8.12)

Pin diagnostic dump z v3.38.8 Cohh session ukázal:

  pinnedBy="Suzunahara" ✓
  bodySegs=9 ✓
  author=null ✗
  authorBadges=0 ✗
  timeText=null ✗

  html=<div class="pinned-chat__highlight-card__collapsed ...">

Root cause: v collapsed stavu (chat hidden/collapsed) Twitch
vůbec nerenderuje author footer v pin card — jen pinner + body.
Extractor nemá co vytáhnout.

Fix: content script před extract detekuje __collapsed class +
klikne expand button (aria-label rozbalit/expand nebo
aria-expanded=false) real-event sequence (pointerdown/mousedown/
pointerup/mouseup/click) — obejde Vue synthetic click resistance.
Twitch card expanduje → author row mountne → extractor dostane
full data. _ucExpanded flag prevent re-click cycling.

Sidepanel render už byl graceful (footer skip když !author
&& !timeText), takže ani nouzovému case nevadí.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User report: chat by neměl být collapsnutý, jen schovaný — všechno
by se mělo renderovat.

Správná diagnóza: width:0 / min-width:0 / max-width:0 triggerovalo
Twitch interní ResizeObserver na chat column. Když Twitch detekoval
0-width, přepnul rendering režim:

- Pin cards → .pinned-chat__highlight-card__collapsed (jen pinner
  + body, bez author footeru)
- Community highlights → compact "!N" badge na playeru místo full
  cards

Mirror scraper neměl co vytáhnout.

Fix: místo width:0 používáme position:fixed + right:-9999px +
height:100vh. Element je mimo viewport (uživatel nevidí) ale
offsetWidth zůstává na natural hodnotě (~340px) — Twitch's
collapse detection nefire-uje, takže všechno se rendruje
full-mode jako by chat byl otevřený.

visibility:hidden + pointer-events:none zajišťují že hidden chat
neblokuje klikání na to co je pod ním.

v3.38.12 expand-click workaround zůstává jako fallback pro
scénář kdy user má vanilla "Sbalit" aktivní.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Correct fix for "pin se nezobrazuje když chat je schovaný":

User's insight: pin zpráva EXISTUJE na Twitch serveru bez ohledu
na to, jestli UI komponenta je mountovaná. Nespoléhat na DOM
scraper — fetch pin data přímo z Twitch GQL.

Implementace:

1. Background: nový FETCH_PINS handler volá
   channel.pinnedChatMessages GQL query. Vrací:
   - pinnedBy (displayName / login)
   - sender (displayName, login, chatColor, id)
   - senderBadges (setID + version, mapované na URL v sidepanel
     přes _twitchBadges cache)
   - content.fragments (text + emote IDs → CDN URL)
   - pinnedAt timestamp
   - pinId

2. Sidepanel: _startPinPoll() volá FETCH_PINS každých 8s,
   transformuje pins do highlight card formátu a pushuje jako
   kind:'pin' do highlights banneru. _handleHighlights merguje
   DOM cards (hype-train/gift/raid) + GQL pins. DOM pin cards
   jsou filtrované out jako redundantní.

3. Channel switch resetuje _gqlPinCards + _lastDomHighlightCards.

4. Hide CSS vrácena na stabilní width:0 z v3.27.4. Pin data
   už nezávisí na tom, co Twitch UI zrovna mountuje — funguje
   i když je chat column "collapsed".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diagnostic Pin log z v3.38.14 (díky FetchPins GQL errors log):

  Cannot query field "pinnedAt" on PinnedChatMessage
  Cannot query field "sender" on PinnedChatMessage
  Cannot query field "senderBadges" on PinnedChatMessage
  Cannot query field "content" on PinnedChatMessage
  Cannot query field "emoteID" on Emote
  Cannot query field "bitsAmount"/"tier" on Cheermote

Schema guess byl špatný. Skutečné schéma:

  PinnedChatMessage {
    id, startsAt (NE pinnedAt!), endsAt,
    pinnedBy { id, login, displayName },
    message {                  # nested!
      id,
      sender { id, login, displayName, chatColor },
      content {
        text,
        fragments {
          text,
          content {
            ... on Emote { id, token }     # NE emoteID
            ... on Cheermote { prefix }    # no bitsAmount/tier
          }
        }
      }
    }
  }

Query rewritten, mapper pulluje z node.message.*. Badges no longer
direct on PinnedChatMessage — sidepanel může padnout zpět na
_twitchBadges cache via sender login (recent-chat mapping).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.38.15 log: "Cannot query field 'message' on type
PinnedChatMessage" — ani message field neexistuje, nejen sender/
content.

Instead of další guess-work nad GQL schema, poslat introspection
query jednou per session: __type(name: "PinnedChatMessage") {
fields { name type { ... } } }. Výsledek se loguje jako Pin tag —
uvidíme VŠECHNA skutečná pole.

Minimal working query teď: id / startsAt / endsAt / pinnedBy
(confirmed funkční z předchozích diagnostic). Sender + content
fields přidáme v další iteraci podle introspection výstupu.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.38.16 log ukázal že Twitch zakázal GQL introspection:
__type(name: "PinnedChatMessage") vrátil {"data":{}}.

Probe loop iteruje 18 candidate field names — každý v
samostatném query. Pole co selže s "Cannot query field"
neexistuje; pole co projde (nebo vyžaduje subselection) se
loguje jako PROBE EXISTS.

Kandidáti: type, text, body, contentText, rawText, html,
messageID, messageId, pinnedMessageID, chatMessageID, sender,
user, author, fromUser, sourceUser, fragments, emotes.

Runs once per session. Další iterace: podle PROBE výsledků
postavíme funkční query pro content + author.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…3.38.18)

v3.38.17 probe log ukázal že všech 17 kandidátů vrátilo:
"Variable \"n\" has invalid value null" — moje query použila
$n ale variables měly { name: channel }. Všechny queries tedy
failovaly PŘED schema check, žádná užitečná info.

Fixy:
1. Query variable přejmenován na $name (match s variables obj).
2. Error classifier: "Cannot query field" = field neexistuje
   (skip), "requires subselection" = object type (log EXISTS),
   čistý success = scalar (log EXISTS + sample).

Next iteration: podle PROBE výstupů postavíme query na sender /
content nebo ekvivalent.

Na dotaz "není toto v Twitch docs": Twitch GQL je NEdokumentované
interní API — jen Helix REST je public. Nejspolehlivější schema
zdroj je Chrome DevTools Network tab na Twitch UI (filter gql,
find pin-related operation, copy query).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.38.18 probe confirmed: PinnedChatMessage v anonymní GQL má
jen scalar fields (id, startsAt, endsAt, type="MOD") +
pinnedBy. Žádné sender, content, message reference.

Merge strategy:
- DOM mirror (TW_HIGHLIGHTS) má přednost — full sender, content,
  emotes, badges. Platí když chat je visible.
- GQL fallback — jen metadata (pinner + time + "Pinned"
  placeholder body). Platí když chat je hidden a DOM pin stack
  je unmountnutý.

Pokud chcete full content při hidden chat: potřebujeme
autoritativní Twitch GQL schéma. User může zachytit z Network
tab na Twitch UI (filter "gql" → pin-related request → copy
payload) a pošleme správnou query.

Probe loop odstraněn — known results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User captured the real Twitch web UI response from Network tab.
Real schema (operation: GetPinnedChat):

  PinnedChatMessage {
    id, type, startsAt, endsAt, updatedAt,
    pinnedBy { id, displayName },
    pinnedMessage {                  # <-- NOT "message"
      id, sentAt,
      content {
        text,
        fragments {
          text,
          content { ... on Emote { emoteID } }
        }
      },
      sender {
        id, chatColor, displayName,
        displayBadges { setID, version }
      }
    }
  }

Query přepsaná na real schema. Mapper pulluje:
- segments z fragments (emote URLs z emoteID)
- sender.chatColor pro author color
- sender.displayBadges → _twitchBadges cache pro URLs + titles
- sentAt jako timestamp

Pin banner má teď full obsah (pinner, author, badges, colored
name, text s emotes, timestamp) bez ohledu na to, jestli je
vanilla chat visible nebo skrytý.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Log: "Cannot query field 'emoteID' on type Emote".

User's browser DOES get emoteID — Twitch UI auto-adds
Client-Integrity header. Our anonymous fetch doesn't → whole
query fails → NO data (not just emote — sentAt, sender, badges
too). That's why v3.38.20 UC banner was still empty.

Fix:

1. Drop `... on Emote { emoteID }` spread. Keep only
   `__typename` on fragment content. Query survives, sentAt +
   sender + displayBadges arrive correctly.

2. Sidepanel resolves emote URL by NAME against local maps
   (twitchNative / channel7tv / global7tv / bttvEmotes /
   ffzEmotes). Unknown emote → render as plain text fallback.

3. Poll interval 8s → 4s. Plus visibilitychange listener fires
   immediate tick on panel focus so tab switching shows fresh
   pin instantly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs shown in user's screenshots:

1. STACKING DUPLICITY (15+ "Připnuto uživatelem Suzunahara"
   cards piled up after ~1 min). _rerenderHighlights merged
   _lastDomHighlightCards + _gqlPinCards. _handleHighlights
   then stashed merged result back as _lastDomHighlightCards.
   Each 4s poll tick = one more pin copy accumulated.

   Fix: _lastDomHighlightCards holds ONLY non-pin cards
   (filter). Pins come fresh each tick from either DOM or GQL
   source, never from cache. Plus _rerenderTag identity so
   rerender msgs skip the caching block entirely.

2. MOD BADGE 2× in author footer. DOM extractPinDetails walked
   authorEl.parentElement up until flex/grid container — but
   Twitch's card root is flex, which includes BOTH the pin
   header (pinner + pinner's mod badge) and footer (author +
   3 author badges). 4 total imgs collected, first mod appears
   twice.

   Fix: authorRow = authorEl.closest('p') — strictly scopes
   to the footer <p class="jPfhdt">. Plus hard dedup on badge
   src URL as a belt-and-suspenders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User report:
- Pin se "collapsne co pár vteřin" (banner se re-mountoval každé
  4s pin poll, ztrácel user's chevron toggle click)
- Po reloadu addon: emoty + badges chybí, až po chvíli naskočí

Fixy:

1. Idempotent rerender v _handleHighlights. Hash zahrnuje
   visible data (kind/text/pinId/author/color/badges urls/
   segment types+URLs/timeText). Pokud hash matches předchozí,
   SKIP DOM re-mount. Žádný flicker, žádný collapse reset.

2. Preserve collapsed state přes re-render. Při teardown:
   iterate přes existující pin wraps, collect (pinKey →
   collapsed boolean) do _pendingPinCollapse. V _buildPinCard:
   restore — pokud wasCollapsed === false, user ho expandnul,
   ne-nastavuj collapsed; jinak default collapsed.

Emotes resolve issue: anonymní Twitch GQL nevrací subscriber
emotes (Client-Integrity gate) → pin body segments mají jen
text name, ne URL. Fallback: DOM scraper (po pár vteřin až
chat subtree mountne) nebo IRC learning dodá real URLs.
Display transition plaintext → full emotes je accepted tradeoff.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Log ukázal: [Pin] extract missing=author with class
"pinned-chat__highlight-card__collapsed". Twitch auto-flipuje
pin card mezi expanded/collapsed każdou pár vteřin. Collapsed
stav nemounne author footer → DOM extract vrátí author=null,
badges=0, timeText=null.

Předchozí DOM-wins logika pak downgradeovala banner na
partial DOM místo GQL fallback. Z plných emotes+badges dolů
na "cohhWow YOU YES YOU... CohhilitionBot odesláno v 18:10".

Nový _mergePinCard(domCard, gqlCard):

  for each field (pinnedBy, author, authorColor, authorBadges,
                  bodySegments, timeText):
    use DOM this tick if non-empty
    else use _lastGoodPinCache
    else use GQL fallback

  _lastGoodPinCache refresh only when DOM extract had complete
  footer (author + timeText + bodySegments all present).

Tím: jednou získaná expanded data drží se přes další collapse
cykly. Emote URLs z DOM zůstávají. GQL author/badges vyplní
collapsed DOM mezery.

Channel switch resetuje _lastGoodPinCache + _lastHighlightsHash.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User report: "druhý render je nejlepší (badge + emoty), jen mu
to trvá než se objeví".

Root cause: DOM TW_HIGHLIGHTS příjde až když MutationObserver
v content script tick-ne na changes — může být pár sekund po
boot než pin card se objeví / mění. Mezitím sidepanel rendruje
jen GQL fallback.

Fix: proactive scan na demand.

1. Content script: nový SCAN_HIGHLIGHTS_NOW handler
   (forcibly resets lastHighlightHash + calls relayHighlights).
2. Sidepanel: _kickDomHighlightScan() sendMessage do všech
   Twitch tabs. Volá se:
   - na _startPinPoll boot
   - každý 4s pin poll tick
   - na visibilitychange → visible

Round-trip content→sidepanel je ~10-30ms vs. několik-sekundový
wait na next MutationObserver tick. Expanded DOM pin (s plnými
emotes + badges) se objeví okamžitě.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…v3.38.26)

User report: "emoty probliknou a hned zmiznou!".

Root cause: _rerenderHighlights vytvářelo msg.cards =
[...non-pin, ...gqlPinCards]. _handleHighlights pak filtroval
kind:'pin' a dostal GQL pin **jako by byl DOM pin**:

  const domPins = domCards.filter((c) => c?.kind === 'pin');
  // ^ contained gqlPin, not actual DOM pin!
  const mergedPin = this._mergePinCard(gqlPin, gqlPin);
  // Both args identical → bodySegments picked from d (=gql)
  // plaintext names, overriding cached DOM URLs.

Každý 4s rerender tick → banner downgrade na plaintext i když
cache měla plné DOM URLs.

Fix:
1. _rerenderHighlights posílá msg.cards = [...non-pin] only —
   NO GQL pins injected.
2. _handleHighlights pulluje gqlPin vždy přímo z this._gqlPinCards[0]
   (separate source od DOM channel).
3. isRerender guard prevent _lastDomHighlightCards overwrite
   během rerender tick (kde msg.cards neobsahuje žádné DOM data).

Timing teď:
- T=0: GQL → render 1 (plaintext, cache empty, DOM not yet)
- T=50ms: DOM → render 2 (FULL URLs, cache fresh)
- T=4s tick: rerender se jen non-pin cards, merge pulls cached
  DOM URLs → render stays FULL. Identical hash → no re-mount.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Po sérii iterací (v3.38.0 → v3.38.26) user explicit potvrdil že
pin banner funguje. Požádal o "checkpoint a ne že to budeš měnit".

Memory lock:
- checkpoint_v3_38_26_pin_stable.md — detailní stable flow
  (fetchPins query shape, _mergePinCard per-field order,
   _rerenderHighlights semantic, _lastGoodPinCache conditions,
   isRerender guard, emote resolve strategy, per-channel caps,
   proactive DOM scan)
- MEMORY.md index: 🔒 marker pro prioritní načtení v další session
- CLAUDE-HISTORY.md Changelog: záznam o zamčení

Pravidla pro další session:
1. Číst checkpoint PŘED jakoukoli změnou pin komponent
2. NIKDY neměnit: fetchPins query struktura, _mergePinCard
   order, _rerenderHighlights cards (jen non-pin), cache update
   conditions, isRerender guard
3. Pokud user chce změnu pin flow, EXPLICITNĚ se zeptat zda
   chápe riziko regrese

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User: "Udělat jméno lépe čitelné, některé barvy jsou blbě
čitelné... celkově tu pinnutou zprávu více fancy, víc výrazně."

Změny JEN vizuální — core pin flow (v3.38.26 stable) se nemění.

CSS:
- Bilaterální gradient border-image (zlaté tóny)
- Radial + linear gradient pozadí
- Top highlight line přes .hl-card.hl-pin::before
- Layered glow box-shadow + 5s pulse animace (hl-pin-glow)
- Pulsing pin icon — 2.6s scale + drop-shadow (hl-pin-icon-pulse)
- Author footer divider — fading amber line přes ::before
- Badges wrapped v semi-transparent chip container
- Timestamp jako pill s borderem a amber-tinted backgroundem
- Body text — větší (14.5px), stronger shadow, 1.7em emotes
- Hover state pro btny: translateY(-1px) + warm glow
- Head label uppercase s letter-spacing

JS:
- readableColor(authorColor) na .hl-pin-author — tmavé hex
  jako #B22222 se zesvětlí pro guarantee contrast proti amber
  pozadí (stejný algoritmus co používáme pro chat usernames)
- Plus stacked text-shadow pro universal readability napříč
  všemi barvami

Core flow (_mergePinCard, _rerenderHighlights, _handleHighlights,
_lastGoodPinCache, isRerender guard, GQL query struktura) zůstává
v3.38.26 stable — respekt user pokynu "nerozhrabávej".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User: badges + author name + timestamp teď všechno na jedné
řádce (předtím timestamp wrap-oval pod).

Fix: flex-wrap: nowrap na .hl-pin-foot, ellipsis truncation
na .hl-pin-author (pro extrémně dlouhé nicky), flex-shrink: 0
na timestamp pill aby se nezmačkl.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User: "chci aby to jméno nebylo zkráceno, klidně použijme to
místo pod obrázkem piny".

- .hl-pin-body padding-left 42px → 14px (recover the empty
  gutter under the pin icon)
- .hl-pin-author: dropped ellipsis truncation, now nowrap +
  flex-shrink: 0 → full name renders no matter how long.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User request: "udělej zase mockup command /uc pin 'Obsah pinnuté
zprávy'".

Pushes a mock pin card into _gqlPinCards + triggers
_rerenderHighlights — goes through the real merge pipeline so
the visual matches actual pins 1:1.

Body tokenization on word boundaries with emote name resolution
(channel 7TV → global 7TV → BTTV → FFZ → Twitch native) so mocks
with real emote tokens (cohhWow, etc.) render as images.

Pinner + author = current user; color picked from
_platformColors.twitch; badges from _myBadgesCache mapped via
_twitchBadges. Timestamp = current local HH:MM.

Plus 'pin' entry in UC_CMDS autocomplete + help message.

Core pin flow (v3.38.26 lock) zůstává nedotčený — jen injection
source.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.38.30 bug: args v _handleUcCommand je raw string (text
po "/uc "), ne array. args.slice(1).join(' ') vracel
".slice" na stringu (= text s prvním znakem odstraněným) —
body byl vždy empty nebo zmrzačený.

Fix: parts.slice(1).join(' ') — parts je split array
vytvořená na začátku _handleUcCommand.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.38.31 stále nefungoval. Root cause: mock pushed do
_gqlPinCards → _rerenderHighlights → _mergePinCard:
  domPin = undefined (rerender path nezahrnuje pins)
  gqlPin = mockPin
  cache = předchozí reálný pin (cohh)

pick priority: DOM → CACHE → GQL. Cache měl full data z
reálného pinu před mock → cache won, mock was never rendered.

Fix: injekt mock jako DOM-highlights card přes direct
_handleHighlights({cards:[mockPin, ...cached-non-pin]}) +
_lastHighlightsHash = '' bust. Non-rerender path:

  domPins = [mockPin]
  isRerender = false
  _mergePinCard(mockPin, gqlPin) — DOM wins everything
  cache refreshes from mock (all fields present)
  banner rendruje mock

Příští real DOM scan nebo pin poll mock nahradí — to je
expected pro mock command.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mock /uc pin teď není destruktivní vůči real pinu. Place do
separate _mockPinCards array; banner stackne real (merged DOM+GQL)
+ mock pin samostatně. Real pin polling pokračuje normálně, mock
auto-expire po 30s.

Channel switch resetuje _mockPinCards + clearTimeout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
….38.35)

Diagnostic dump z UC-pinned zprávy ukázal:

  <p class="iQZxnA">
    [textNode "Připnuto uživatelem "]
    <span><div><img mod-badge></div></span>
    [textNode "Jouki728"]
  </p>
  <p class="pinned-chat__message">peepoHey ⠀</p>

V card-root div je textContent = "Připnuto uživatelem Jouki728peepoHey ⠀"
(38 chars, pod 60 limit). Greedy regex [\p{L}\p{N}_-]+ zachytil
"Jouki728peepoHey" jako pinnedBy → bug "Jouki728pee...".

Fix #1 (pinnedBy):
- Scan DIRECT child text nodes elementu (skip descendant text)
- Pokud první přímý text matchuje label, pinner je extracted z
  same-element direct text za labelem
- Tight regex [A-Za-z0-9_]{2,25}\b (Twitch username chars + length cap)

Fix #2 (body):
- Selector [class*="pinned-chat-message"] (hyphen) NEnematchoval
  real Twitch class "pinned-chat__message" (double underscore)
- Přidány .pinned-chat__message + [class*="pinned-chat__message"]
- Body extract teď najde correct element s emote fragments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User report:
1. peepoHey emote nezresolvněn (zobrazí se jako text)
2. Ostrý okraj místo zaoblených rohů

Fix #1 (emote resolve):
Twitch's pin DOM keeps body as <span class="text-fragment">peepoHey ⠀</span>
plain text. 7TV/BTTV/FFZ Vue replacer doesn't penetrate pin subtree
(only chat-line containers). Body segments arrive as text type.

_buildPinCard teď tokenizuje text segmenty (split na whitespace)
a resolve každý token proti emote library order:
twitchNative → channel7tv → global7tv → bttvEmotes → ffzEmotes
→ kickNative → ucEmotes. Resolved emotes render jako <img>,
unresolved fallback na linkified text. Plus strip UC marker
(Braille blank ⠀).

Fix #2 (rounded corners):
border-image: linear-gradient(...) v Chrome ignoruje
border-radius (spec implementation-defined behaviour). Rohy
byly ostré navzdory border-radius: 10px.

Nahrazeno: solid amber border + multi-layer box-shadow
(combination outer glow + inner ring + bottom-edge accent)
which together replicate the gradient ring effect WHILE
border-radius drží.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User: peepoHey stále renderoval jako text.

Bug: emote maps (channel7tv, global7tv, bttvEmotes, ffzEmotes,
twitchNative, kickNative, ucEmotes) ukládají URL **string přímo**:

  this.channel7tv.set(emote.name, url);
  this.bttvEmotes.set(e.code, `https://cdn...`);

Můj v3.38.36 resolver dělal entry?.url — accessing .url na
string vrací undefined. Falsy check failed → emote rendered
jako text vždy.

Fix: const url = resolveEmote(name); if (typeof url === 'string'
&& url) — rendrue img s tím URL. Stejný fix v /uc pin mocku.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User: po klik UC pin button se objevily 2 karty — jedna stará
generic, druhá nová fancy.

Root cause: 2 paralelní pin paths.
- Legacy v3.37.0: PIN_MESSAGE mutation → _showPinnedBanner →
  separate #pinned-banner element + _startPinWatcher
- v3.38.x: FETCH_PINS poll → _gqlPinCards → #highlights-banner

Oba renderovaly stejný pin paralelně.

Fix: po úspěšné PIN_MESSAGE mutation:
1. _hidePinnedBanner — schovat legacy element
2. Trigger immediate FETCH_PINS (instead of waiting up to 4s
   for next poll tick)
3. Pin se objeví jen v fancy #highlights-banner

Plus refactor: _pinFromGql(p) extracted as method (jeden
zdroj pravdy pro GQL pin → highlight card transform). Volá ho
periodic poll tick i post-mutation fast-fetch. Plus opraveno
entry?.url → typeof url === 'string' (v3.38.37 fix consistent
applied here).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jouki jouki merged commit f3e1f07 into master Apr 15, 2026
1 check passed
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.

1 participant