Mephisto 3.x — 2026 site adapters, modern engine lineup, four-player chess - #37
Open
IchNukeDichWeg wants to merge 221 commits into
Open
Mephisto 3.x — 2026 site adapters, modern engine lineup, four-player chess#37IchNukeDichWeg wants to merge 221 commits into
IchNukeDichWeg wants to merge 221 commits into
Conversation
…toplay Engines: add Stockfish dev (2026-06) and SF18 small-net WASM builds from @lichess-org/stockfish-web 0.4.0; drop the broken SF6/SF16/SF17-79 builds (SF17-79 crashed with unaligned/OOB access on some machines). Default is now Stockfish dev. (SF18 full-net is omitted here because its net exceeds GitHub's file-size limit; the dev build is an SF18 build and covers it.) Detection: follow Lichess's obfuscated move-list DOM -- real-time games nest it under .col1-moves, correspondence games directly under <app>; scrape the full live move list instead of trusting the churning selected-move class; drop the end-of-game result/status element so it is not parsed as a bogus move. Turn: derive side-to-move robustly (start position = White, move-count parity fallback, full live-game scrape) so autoplay fires on the right turn and plays White's opening move. Crash safety: validate scraped positions before feeding the engine (a king-en-prise / missing-king / back-rank-pawn FEN OOB-crashes the WASM engine) and skip mid-animation piece scrapes. Autoplay: 15s watchdog on the move-in-progress flag; ignore invalid click coordinates. remote-engine.py: support engines without MultiPV and surface opening-book (depth-0) moves, so pygin/UCI backends work. Defaults: engine=stockfish-dev, search 500ms, fen-refresh 50ms, threads 4, memory 256MB, autoplay on, evaluation shown, threat/ponder off. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a new position arrived while the engine was still searching the previous one, the 'stop' sent by on_new_pos made the engine flush a bestmove for the OLD position. It arrived after 'turn' was already updated, so if the old search was for the opponent's side (they replied within the search window), that stale move was autoplayed as ours -- stray board clicks that could select a piece and corrupt the next real automove. Now all output of a stopped search, up to and including its flushed bestmove, is discarded; the flag resets on engine (re)init so a crashed engine can't eat the fresh engine's first result. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- stockfish-17-79/ restored byte-identical from upstream (blob oids unchanged) and rewired in popup.js/general.html; removed from the REMOVED_ENGINES migration list. - stockfish-18/ (full dual-net build from @lichess-org/stockfish-web 0.4.0) added: its big net nn-c288c895ea92.nnue is 104MB, over GitHub's 100MB blob limit, so it ships as three <50MB .part0..2 chunks. fetch_nnue() in popup.js stitches chunks at load time and falls back transparently for whole-file nets; verified reassembly sha256 == c288c895ea92... (the net's content-addressed name). - .gitattributes: keep the new nets as regular blobs (public forks cannot upload new Git-LFS objects). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… bar, continuous analysis - Draggable floating panel injected into the page replaces the anchored popup (toolbar icon toggles it); it stays open while playing, making the old 'Inspect Popup' workaround obsolete. Extension pages now ship COOP/COEP so the engine's SharedArrayBuffer threads work inside the embedded frame; engine files are web-accessible for its workers. - Quick Settings sidebar: live Autoplay/Premove/Puzzle/Help toggles + engine/threads/memory/lines/search/fen/think/move settings. - Orientation-aware eval bar next to the mini board. - Help mode: mirrors all analysis arrows onto the site's board while evaluating continuously; user plays the move themselves. - Continuous analysis (go infinite) whenever autoplay is off. - Safe premove: replies certified for stability across depths 6/9/10+; instant reply only when the opponent plays the predicted move, real premove only when provably unable to misfire (forced moves, recaptures). Guarded against stale stopped-search bestmoves. - 'From Position' games (custom starting position) supported. - New defaults; version 3.0.0; README updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, live depth, 2026 DOM fixes Supersedes AlexPetrusca#30. Full changelog in the PR description. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prevents fen-poll pile-ups when the scrape round-trip is slower than the poll interval, with a 500ms self-heal so a dropped reply (content-script busy performing a move) can never wedge polling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Dark Mode switch under Appearance settings; themes the options pages and the floating panel (single class toggled from one setting). - Getting Started updated for the floating panel (drops the obsolete 'Inspect Popup' workaround) and documents Help Mode and Premove. - Footer copyright 2025 -> 2026. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…anded - The content-script now always answers a fen poll (with 'no' while it is mid-move) so the popup's in-flight poll guard clears immediately instead of waiting out the fallback timeout -> the board updates promptly again. - simulateMoveVerified: after autoplaying a move, confirm the move list actually grew and retry (up to 2x) if a click silently failed. The move-count check can't double-move -- if a move registered we treat it as success even if the opponent already replied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A premove is clicked during the opponent's turn -- the site queues it and it never grows the move list until they move, so the move-count check false-failed and retried, clobbering the queued premove. Verification now runs only when it is our turn to move; premoves are played once and left alone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Never auto-move on a stale automove message if Autoplay has since been turned off. Harmless in normal play (autoplay/premove both require the flag on). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ition A certified instant reply is now gated by premove_reply_playable(): it must move OUR piece and, on our turn, be fully legal right now. Guards against a stale/mismatched reply making the extension click the opponent's move or an illegal move (then thrashing the retry). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The debugger click attached to the active tab, so a move firing while you were on another tab (e.g. chrome://extensions) dispatched there and failed with 'Cannot access a chrome:// URL'. Clicks now target the content-script sender's tab, so autoplay works regardless of which tab is focused. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Only autoplay/premove a move that moves OUR piece and (on our turn) is legal in the current position -- never the opponent's move if the turn was mis-scraped. - Before each move's clicks, click a safe empty square (one the moving piece can't reach) to clear any stale selection, so a retry's from-click can't deselect an already-selected piece and no-op the move. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simulateMoveVerified re-read the move-list length at the start of every retry, so a move that registered LATE (chess.com board animation slower than the fixed 250ms wait) was seen only after the baseline had already advanced -- the retry then replayed into a changed board and still logged 'failed to register'. Capture the baseline once before the first attempt and thread it through retries, and poll up to 1.5s for the count to grow instead of a single snapshot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… re-derivation The move-registration check (simulateMoveVerified) was gated by the content-script's isOurTurn(), which re-derives whose turn it is from lichess DOM highlights. That throws on boards where the piece selector differs (e.g. the analysis board), so isOurTurn() returned false and verification was SILENTLY SKIPPED -- a failed move was never retried and no warning was logged. The popup already knows the turn authoritatively; it now decides real-move-vs-blind-premove from the position's side-to-move and passes a flag in the automove message. Blind premoves (played during the opponent's turn) still skip verification as before. Also drop the per-poll DIRECT/INDIRECT/FULL cache-path logs and the per-scrape position log -- they fire ~20x/second at the default refresh and bury the move-verification warnings in the console. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scale 0.85 to 0.75 (the popup is a fixed 548px layout, so scaling shrinks it uniformly without clipping the board or the quick-settings column) and pin right: 0 instead of 12px. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ark mode Minimize button in the panel title bar hides the overlay via visibility:hidden and drops a small restore badge. The iframe stays in the DOM and running, so autoplay / premove / help mode keep working while minimized. This also fixes autoplay clicks that were being eaten by the panel: clicks are dispatched at screen coordinates, so whenever the panel sat over a destination square it got the click instead of the board -- minimizing removes it from hit-testing. Dark mode: deeper cohesive slate palette, dimmed secondary text, a titled/ underlined settings header, teal focus rings on inputs, a dark slim scrollbar for the settings panel, and flattened action buttons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New 'Eval Bar' quick-setting draws a vertical eval bar just to the left of the game board, styled like the popup's bar (dark = black's share, white = white's share) with the score shown inside it chess.com-style, on the winning side's end. Reuses the existing hint-overlay plumbing: the popup computes the numbers in update_eval_bar and pushes them to the content- script on every eval update; the bar is absolutely positioned over the board, orientation-aware, pointer-events:none so it never blocks a click. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two tab-isolation fixes: - The popup sent to the globally-active tab and the content-script broadcast fen/config/click to every tab's popup, so a background tab's Mephisto drove the foreground tab (e.g. turning on help there). The content-script now learns its own tab id from the background and passes it to the popup iframe via a ?tab= param; the popup messages only that tab and ignores any content-script message from a different tab. Falls back to the old behaviour if the id isn't known yet. - Closing the panel (X) removed the iframe but left the eval bar / hint arrows drawn on the board. removeOverlay() now clears them too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Minimize now hides the panel with opacity:0 + pointer-events:none instead
of visibility:hidden, so the iframe stays full-size and in-viewport and
Chrome never throttles its timers -- autoplay/premove/help keep running at
full speed exactly as if open. Closing with X still removes the iframe and
stops everything until reopened.
- Options-page dark mode: lift the dim field labels and make the live slider
values ('Threads (6)', 'Memory (128 MB)', ...) white so they're readable,
plus bordered section cards, an accented header underline, and a toned-down
range track.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A mainline Stockfish (dev/18/17/11) that isn't told it's playing Chess960 treats the game as standard chess and mishandles castling whenever the king or rooks aren't on their standard files. Send 'setoption name UCI_Chess960 value true' during setup when the variant is fischerandom and the engine is a mainline SF, so SF -- far stronger than the SF14-based Fairy-Stockfish -- can be used correctly for 960. Fairy-Stockfish is unchanged: its 'fischerandom' UCI_Variant is a 960-typed variant that already handles it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The variant was only settable on the full options page, so panel users had no way to tell Fairy which variant a game is -- it silently analysed every variant as standard chess (e.g. a Three-check game evaluated as winning while it was lost by checks). Add a Variant dropdown to the Quick Settings panel, shown only when Fairy-Stockfish is the engine; changing it reloads so the matching NNUE loads and UCI_Variant is set. New nets slot in by adding one <option>. Next to it, a detect button reads the variant off the lichess game page (variant name links to a stable /variant/<key> URL) and switches to it. Panel is ~40px taller so the extra row fits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Drop the Stockfish 17 NNUE engine: removed from both engine dropdowns, the engineMap and the wasm-engine list, added to REMOVED_ENGINES so a stale selection migrates to the default, and deleted its ~79MB of engine files. Remaining: SF dev / SF 18 / SF 18 Small / SF 11 HCE / Fairy SF 14 / Remote Engine. - Options-page dark mode: drop the hard 1px border + shadow + header underline that made each section look like a stray boxed-off frame (the section background alone now groups it), and actually darken the range slider track (materialize's bright default line) so only the teal thumb stands out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When minimized the panel is opacity:0 + pointer-events:none, but the drag handler's mouseup leaves an inline pointer-events:auto on the iframe, which overrides the wrapper's inherited none -- so after ever dragging the panel, the invisible frame kept eating clicks meant for the page underneath. Set pointer-events:none explicitly on the iframe when minimizing (restore it on un-minimize) so you can click through to whatever is under the panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bright slider line in dark mode wasn't the ::-webkit-slider-runnable-track pseudo (that's transparent) -- materialize puts border:1px solid white on the range ELEMENT itself, and that white border was the visible line. Overriding the track pseudo did nothing. Recolour the element border to a subtle #45454b so only the teal thumb stands out. Verified in-browser. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dark-mode sliders were a harsh light line (materialize's 1px white border on the range element). Reset the control and paint a real progress track: a teal fill up to the thumb + a subtle dark remainder, via a gradient on the element background driven by a --fill custom property. options.js sets --fill from each range's value (on page render + live while dragging). Verified in-browser. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rack - Drop the boxed-off card look: html.dark .big-section is now transparent (no background/border/shadow/radius), so settings sit flat on the page. - The white bit past the slider thumb was materialize's ::-webkit-slider-runnable -track rendering over our gradient -- make the track pseudo transparent so only the teal-fill element background shows. Verified in-browser. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chess.com runs variants at www.chess.com/variants/<slug>/game/<id> with a React 'TheBoard' component that shares nothing with the main site's DOM -- the extension saw 'No Chess Game Detected' on every variant game. Now: - getBoard/getMoveContainer/getMoveRecords recognize the variants board (.TheBoard-layers, .moves-moves-list, one SAN per .moves-table-cell.moves-move, filtered to real moves so placeholder cells and result markers don't parse). - scrapePositionFen ships the SAN list; Fairy-Stockfish rebuilds the position from UCI_Variant + SANs through the existing variant path. - The variant 'detect' button now also works on chess.com: detectVariant() dispatches per site, reading the chess.com variant from the URL slug. - Autoplay works on variant boards. The click pipeline was already geometry- based and board-agnostic; the one missing piece was getOrientation() -- the variants board has no coordinate labels or flip class, and piece data-color codes are per-game numbers. getChesscomVariantsOrientation() classifies the two sides RELATIVELY: decode each colour's SVG sprite, the lighter-filled side is White; the board is white-oriented when White sits lower on screen. Verified against a live game: all pieces' squares mapped to their exact screen centers. Variants supported end-to-end (detection + analysis + autoplay): 3check, kingofthehill, crazyhouse, antichess, atomic, horde, racingkings -- the ones the bundled chess.js can reconstruct. Promotion popups and crazyhouse drops on the variants UI are known gaps (watchdog prevents any hang).
The panel used to learn about position changes by sending {queryfen} every
fen_refresh ms (10ms default), each request a full DOM scrape with forced
layout flushes -- 100 scrapes/second, for the lifetime of the tab, to observe
a board that changes a few times a minute. With the 3.x panel staying open for
whole games this ran for hours and taxed the page it was supposed to read.
Now the content-script PUSHES positions when the DOM actually changes:
- A MutationObserver on document.body (childList + class/style attributes;
characterData deliberately excluded so clock ticks can't wake the pipeline).
- Mutations inside Mephisto's own overlays are filtered out -- dragging the
panel or redrawing the eval bar can never feed back into the pipeline.
- 30ms debounce collapses a mutation burst (a piece animation is dozens of
mutations) into at most ~33 samples/s, always ending with one settled scrape
~30ms after the burst's last mutation; mid-animation samples are rejected by
the scrapers and never pushed.
- Dedupe on an orientation|scrape key: the panel only hears about real changes.
- Pushes pause during automoves and catch up immediately after (including the
15s watchdog path); a config push resets the dedupe so a (re)opened panel
syncs instantly.
- Pushes reuse the exact {dom, orient, fenresponse} shape of the old poll
replies, so the panel consumes both through one code path.
- The {queryfen} poll survives as a fallback only, clamped to >=1s -- a saved
10ms fen_refresh from an old install can't reinstate the stampede. UI renamed
to 'Fallback Poll'; fresh-install default is now 1000ms.
- Puzzle mode's reply-confirm loop gets a fixed 50ms cadence instead of
piggybacking on fen_refresh.
At idle this is 0 scrapes/s and 0 forced layouts/s (plus the 1/s fallback);
detection latency is mutation+30ms instead of being bounded by the poll round
trip. Verified by a jsdom harness driving the shipped content-script: 16
assertions covering initial push on config, dedupe, burst coalescing, overlay
filtering, automove suppression + catch-up, transient-'no' rejection, and the
intact queryfen fallback. All pass; node --check on every modified file.
Four changes, all of them answers to the same problem: a move that does not happen looks exactly like a bug, and the evidence was either absent or hidden. WHY. Every path that ended in "no move" ended in silence. They now end in one sentence in the panel, under the readout -- autoplay off, tab not focused, board moved on, a move already in flight, a board that is not playable, a square that is not on screen. dropMove() traces AND tells the panel; the seven drop sites all go through it. The four-player skip says which gate stopped it, including the case where the seat could not be read. VERBOSE LOGGING. The trace is quiet while the tab is focused unless Premove happens to be on. That is the right default and the wrong thing to have to guess at, and it hid the cause of a bug for three sessions. Settings -> Diagnostics -> Verbose Logging forces it on, live, no reload. COPY DIAGNOSTICS. A clipboard button in the panel: version, browser, bundled assets, connected hosts, granted permissions, what was detected, why the last move was or was not played, the toggles that are on, and the last 200 trace lines from the worker's ring. Deliberately carries no full URL, no query string and no extension id, so it can be pasted into a public issue without a second thought -- the ladder asserts all three. SCRAPE WATCHDOG. Above 60 scrapes/second -- a rate real play never reaches -- scraping backs off for two seconds and says so once. A mutation loop pinning the main thread is how a browser gets wedged with nothing in the log but thousands of identical lines. background-script.js: a bounded trace ring and the report that reads it content-script.js: verbose gate, the scrape watchdog, dropMove at every drop site popup.js/popup.html/popup.css: the reason line and the clipboard button, light and dark general.html/general.js: the Diagnostics section and the Verbose Logging switch src/i18n/locales/*.json: five strings in fourteen languages Type: improvement
STAGED. Every file goes to .mephisto-staging first, so a complete copy of the new version is on disk before anything in place is touched. True atomicity is not available -- the extension id comes from the folder path, so the folder cannot be swapped out from under itself -- and calling this recoverable rather than atomic is the honest description. UNDOABLE. The files an update is about to replace are copied to .mephisto-backup/<version> with a manifest naming them, and the ones it ADDS are recorded separately so rolling back deletes them rather than leaving two builds mixed together. Roll Back appears in Settings only when there is something to go back to. One generation: what you want is the version that worked ten minutes ago. RESUMABLE. A staging folder left behind means the move step did not finish. Settings offers to finish it from disk -- the bytes are already there and already verified -- and takes its own backup first if the interrupted run never got that far. It will not overwrite a good backup with a half-updated folder, which would destroy the only copy of the version being restored to. WHAT CHANGED. The update check now carries the release's first sentence, and the panel shows it once after an installation: "Updated to v3.1.217 — ...". It outranks both the incomplete-install warning and the next-update notice, because straight after an install that is the only thing worth saying. Verified on a real filesystem against the published 3.1.216 archive: staging leaves the install untouched, the backup holds the OLD bytes and never the engines, the install lands without disturbing files the archive does not contain, and rolling back leaves the folder BYTE-IDENTICAL to before the update. The interrupted-install path completes to the same result. updater.js: staging, backup, rollback, rollbackInfo, pendingInstall, finishStaged background-script.js: updateCheck carries the release name and its first sentence popup.js: the what's-new note, and one UPDATE_REPO_SLUG instead of three copies of the url general.html/general.js: Roll Back and Finish Interrupted Update, shown only when they apply src/i18n/locales/*.json: five strings in fourteen languages Type: new idea
The button row is absolutely positioned by id on a 45px pitch, and its own comment does the arithmetic: five icons plus FEN/PGN is exactly what fits in a 378px column. A sixth had no rule at all, so it fell out of the layout, took Materialize's default filled teal, and landed on the board. Reported as "horrible placement", which it was. It is now D on the game page -- rebindable under Hotkeys, beside Copy FEN and Copy PGN, which is where someone would look for it -- and a Copy Diagnostics button in Settings, for when the panel is not open. The hotkey confirms itself in the reason line, since there is no longer a button to flash a tick on and a shortcut that appears to do nothing is worse than no shortcut. The ladder now asserts the toolbar still holds exactly its five, and the live panel check asserts no button is visible outside it -- the class of mistake that put one on the board in the first place. popup.html: the stray button removed popup.js: copy_diagnostics() as a function, bound to the hotkey, confirming in the reason line config-store.js: default binding 'd' general.html/general.js: a Copy Diagnostics button, and the hotkey listed and rebindable src/i18n/locales/*.json: two strings in fourteen languages Type: bug fix
A hotkey is not something people use. Copy Diagnostics is now a labelled button in the panel's Engine tab, confirming with a tick on the button itself. Not the toolbar row: five icons at a 45px pitch is exactly what fits there, which is what put a stray one on the board last time. The hotkey stays wired, since it is one line, but the button is the way in. THE REASON LINE WAS TOO CHATTY. Four of the seven cases were removed after seeing them in use: "not your turn" fired on every four-player ply, "Autoplay is off" says what the toggle already says (and could barely fire anyway -- the panel does not send a move with autoplay off, so it needed the two config copies to disagree), "this tab is not focused" cannot be read by definition, and "a move is already being played" is transient and self-correcting. All four are still traced. What is left is rare and actionable: a four-player board that is not playable, a move the board cannot understand, a square not on screen, and a position that moved on mid-search. It was also off-centre, because everything in that flow spans the full 378px panel while only the left 188px is visible beside the settings column. Same 190px clearance the notices use. popup.html/popup.css: the Copy Diagnostics row, and the reason line inside the visible column popup.js: the button, and the four-player lane no longer narrates every ply content-script.js: three drops back to trace-only Type: improvement
MEASURED: the panel payload was 973,659 bytes of CSS, of which 784,724 was base64 for the wood,
marble and newspaper board textures. Only one of the twelve themes can be on screen at a time, and
nine of them are flat colours needing none at all -- so on the default theme every byte of that was
waste, shipped over sendMessage and parsed into the shadow root on EVERY open of the floating panel.
The toolbar popup never paid it: it is a real page and loads its own CSS from disk.
The panel now says which theme it is about to use and gets that texture, or none.
default (brown) 973,659 -> 189,909 bytes
wood selected 973,659 -> 538,519 bytes
first open, cold worker 145ms -> 82ms
second open, warm 11ms -> 5ms
FOOTPRINT: a texture that is not carried is replaced with `none`, not left as
url(/res/chessboards/wood.jpeg). Left alone, that resolves against THE SITE if the rule is ever
applied -- a 404 the page can see in its Resource Timing, which is the exact thing this whole
asset-shipping design exists to avoid. The ladder asserts no /res/chessboards/ reference survives in
either case, and that the neutralising happens before the remaining refs are collected.
The cache is keyed by theme, so switching board rebuilds rather than serving the wrong one.
background-script.js: buildPanelAssets takes the theme; inlineCssUrls carries one texture at most
content-script.js: the panel asks for the theme it is about to use
Type: improvement
Reported: ten seconds of nothing on screen after a browser restart, then switching to the toolbar popup did nothing either. Both modes go through the service worker, so the wait was there -- but toggleOverlay awaited the assets BEFORE creating any visible element, so the entire time there was no frame, no spinner and no error. Indistinguishable from the extension being broken, which is how it was reported. A placeholder now goes up before anything is awaited, styled inline because the panel's own CSS is exactly what is still being waited for. It carries the panel id, so clicking the icon again dismisses it. After 2.5s it stops saying "starting" and names the reason -- the background worker can be slow after a browser restart. The request is time-boxed at 20s, because sendMessage to a worker that never answers hangs for as long as Chrome likes. Every failure path now writes into that frame instead of into a console nobody had open, and traces ungated so it reaches the diagnostics report. This does NOT explain the ten seconds; it makes them visible and survivable. The stall itself needs a trace from a real occurrence. content-script.js: a placeholder before the await, a slow-start message, a timeout, visible failures Type: bug fix
PROMOTION is no longer left to you. chess.com opens a 2x2 panel over the board -- queen and bishop on the top row, rook and knight beneath -- and the clicker now picks from it. FOUND BY SHAPE, NOT BY CLASS NAME: every class on that board is generated, and a selector guessed from a screenshot is one that clicks the wrong thing the day it changes. It looks for a small container over the board with exactly four similarly-sized children in two rows of two. If nothing matches that description NOTHING IS CLICKED -- the move is already played, so leaving the piece to you is what happened before, and a wrong guess must never become a wrong piece. Eight cases pin the shapes it must refuse: too large, three children, a single row of four, one nowhere near the board. ELIMINATION was worse than untested, it was wrong. The turn came from counting filled cells in the move table and taking count % 4. chess.com lays that table out as four columns in seat order, so once a seat is knocked out its column stops filling, every later round holds three moves instead of four, and the modulo drifts one seat further out per round. Reading the POSITION of the last filled cell instead is correct with or without an elimination, and gives the identical answer while all four are alive (verified against the measured live case: 20 cells, Red to move). The logic moved into a pure function so a synthetic elimination can be driven without a DOM -- which is the only test it will ever get, since no real game has been seen past one. MULTI-PV was one option away the whole time: Tetrarch declares MultiPV 1-64 and emits one `info ... multipv N` per line, and the host already keys them by rank and returns them in order. The panel now asks, and draws an arrow per line in the same palette and widths the 8x8 board uses, deduped because transposing lines share a first move. The ladder caught my promotion wait building its deadline out of its own step size -- the shape it forbids outright, because Chrome clamps timers to one a second in a hidden tab and a summed deadline is then out by 20x. It uses a real clock now. content-script.js: promote4PC found by shape; fourPCTurnFrom, elimination-correct popup.js: MultiPV sent to Tetrarch, one hint arrow per line Type: new idea
The engine lines were reaching the PAGE board and nowhere else: the panel's own 14x14 board still highlighted only the best move, and the eval list under it -- the coloured legend the 8x8 board gets -- was never populated at all. Three different amounts of information about one search. The arrow specs are built ONCE now and used three times. The 14x14 board takes a list rather than a single move, draws them back to front so the best sits on top, and uses each line's own colour and width in one svg rather than one per arrow. The eval list gets a four-player renderer: no chess.js for a 14x14 board means no SAN, so the moves are the coordinates the rest of this lane already uses, and the score is normalised to YOUR team the same way the readout is -- otherwise a line reads +3 and -3 on alternate plies. SPACING, both reported. The Copy Diagnostics row wrapped into two cramped lines because a plain .qs-row carries no layout of its own: the toggle rows bring their own flex and the select rows are laid out by a full-width select. It is label-left/button-right on one line now. And the settings page title sat directly on top of the first section heading, since `#title` has no bottom padding and every section only spaces itself from the one BEFORE it -- the first had nothing above it. Measured in a real browser: the row is on one line, and there are 19px under the page title. panel-board.js: highlight() takes a list; one svg, best drawn last popup.js: fourpc_arrow_specs and render_alt_lines_4pc, shared by both boards and the list popup.css: the diagnostics row on one line options.css: air between the page title and the first section Type: improvement
A queen promotion picking the queen proves index 0 is the top-left cell FOR A SEAT AT THE BOTTOM -- Red, in the game it was tested in. It does not prove the picker keeps that layout for the other three seats: chess.com rotates everything else with the board, and if it rotates this too the order is wrong for Blue, Yellow and Green. The symptom would be a QUEEN promotion picking something else while Red kept working, which is worth knowing before it is puzzled over. content-script.js: note what is confirmed and what is assumed, where the order is declared Type: docs
The promotion picker is wired, so it no longer leaves the piece to you; multiple lines are drawn on both boards with a score list beneath; and the roadmap item still listed two blockers that are gone. The settings reference gained the diagnostics pair. README.md: four-player promotions and multi-PV, the roadmap item, a Verbose Logging row Type: docs
Carries everything since 3.1.216: the diagnostics cluster, updates that are undoable and resumable, four-player promotions, elimination-correct turn order and multi-PV, a panel payload cut by 79%, and a floating panel that can no longer open into silence. manifest.json: 3.1.216 -> 3.1.217 Type: tooling
Clock Mode paces the engine's SEARCH. Nothing paced the simulated human delay, so in a ten-second scramble a move still spent a fixed think pause plus 400ms of cursor travel, which is exactly when it cannot be afforded. Its own opt-in rather than a change to Clock Mode: that toggle has always meant "pace the search", and widening it would change what it does for everyone already using it. Off, the timing object is returned untouched. popup.js: clock_move_budget_ms split out ungated, clock_pace_timing, fresh_timing routes through it popup.html: Pace to Clock row after Mirror Time config-store.js: default hotkey k general.html: Pace to Clock row in Engine settings general.js: form element, rebind label and order i18n/locales: panel.pace_to_clock + its tooltip, 14 languages Type: new idea Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A new options page that takes a PGN -- pasted, from a file, or fetched from chess.com's public archive -- and reviews it with the extension's own engines. Every position is searched once, so the score before a move and the score after it come from the same search at the same budget. Accuracy, move classification, alternate lines, an eval graph, think time from the clock comments, an optional Maia pass for how expected each move was, and a set of fair-play MEASUREMENTS that deliberately refuse to add up to a verdict. Nothing is uploaded and the report exports as one self-contained HTML file. options/pages/review/review.html: the page options/pages/review/review.css: its styles, on the options page's own variables options/pages/review/review-core.js: PGN reader, eval bookkeeping, accuracy, indicators options/pages/review/review.js: engine drivers, rendering, export options/options.html: nav entry, panel-board + review-core, chessboard.css i18n/locales: nav.review + the page's strings, 14 languages Type: new idea Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A hand-maintained constant with a comment saying "bump on every content-script change". It sat at 3.1.213 through eighty-one commits to that file, so the page console line and the stuck-scraper trace both named a build that had not run since v3.1.213. A version marker nobody can trust is worse than none, so it reads the manifest instead. content-script.js: MEPHISTO_BUILD comes from chrome.runtime.getManifest() Type: bug fix Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A search that outlived the tab it was started from was the real one: the worker frees a panel's engine when its tab closes, keyed by tab id, and this client is deliberately not one -- so closing the options page mid-run left a multi-threaded search burning cores with nobody watching it. review.js: dispose the engine on pagehide; thread default from the machine; DOM writes survive a page swap review-core.js: one unanalysed position no longer renders the whole game's accuracy as NaN; unexported the helpers with no caller review.html: dropped two ids nothing reads Type: bug fix Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README.md: Game review section, Pace to Clock beside Clock Mode, settings-reference row, shipped list docs/game-review.png: the report on the sample game Type: docs Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The trace ring lives in the worker's memory, so when the WORKER is the thing that was asleep -- the ten seconds of nothing after a browser restart -- the ring is empty by definition and Copy Diagnostics explains nothing. The startup marks are persisted instead, so the next report describes a cold start the worker did not live to describe. Measured on this Mac: a genuine worker restart runs the whole script body in 262ms, while the panel open on that same restart took 3,405ms -- so the cost is Chrome spawning the worker, not anything in here. First install is different again (offscreen document +18s), which is exactly the sort of thing that should be a number rather than a hunch. background-script.js: WORKER_T0 + per-import marks, persisted to storage, shown in the report content-script.js: the panel times its own round trip to the worker, traced ungated Type: tooling Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Declared inside the try and read from the catch, where a block-scoped const does not exist -- so the one path whose entire job is to report a failed start threw its own ReferenceError instead of the message it was holding. Stamped above the try, where both paths can see it. content-script.js: assetsAsked is stamped before the request, not inside it Type: bug fix Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t that exports itself Nine changes asked for after the first round of use. The human model moves up beside the engine, where the choice actually belongs, and now reports Maia's OWN RANKING of the played move rather than a yes/no -- "the human model had this fourth" says something a boolean cannot. Maia 1 gains the bands it actually ships, up to 2200. Native engines are no longer locked out of depth mode: the request carries both budgets, so a host that understands `depth` uses it and one that predates the field ignores it and uses `time` exactly as before. The fair-play section is the other half. The headline rate now excludes book moves, forced moves and recaptures -- finding the only legal recapture is not evidence of anything, and counting it is what makes a naive match rate read 80% for everyone. Added: per-phase rates, the longest unbroken engine streak, how uniform the accuracy is, whether the longer thinks went to the harder positions, and the engine-said-yes / human-model-said-no count. Four levels now, with a key that is printed from the same list the levels are assigned from. The export is no longer a second, simpler renderer that drifted from the real one within a day. It clones what is on screen, inlines the three stylesheets and the twelve piece images, and adds the full move table the page shows one row at a time. No script, nothing to fetch. review.js: Maia ranking, native depth, arrows, phases on the graph, titles, DD.MM.YYYY, export review-core.js: the divider ported for phases, date formatting, the wider indicator set and its levels review.html: human model beside the engine, bands to 2200, the level key review.css: first-section spacing, level key, move table, two columns down to 900px uci-native-host.py: an analyse request may carry a depth Type: improvement Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Lichess opening explorer answers 401 at its proxy, before the application sees the request -- both hosts, both databases, with and without a token, from a plain unauthenticated fetch. Whether that is an auth requirement or a network block is not something this code can find out or fix. Naming an opening never needed a server anyway: the answer is a property of the position. This is lichess-org/chess-openings (CC0, a collection of facts in the public domain), 3,810 named lines replayed once and keyed on the POSITION rather than the move order -- so a transposition is named correctly, which a move-list lookup cannot do. Offline, instant, and one fewer thing that can go down. openings.json: 3,810 openings keyed by position Type: new idea Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…runs The fair-play section ended in eleven numbers and no summary, which is not more honest than a summary -- it just moves the summarising to whoever has less information. It now leads with one estimate per player: a weighted vote over the evidence lines, with the weights set by how hard a line is to reach by playing well, plus how much the estimate is worth given what the game actually carried. It is not a probability and says so. Human likeness is a second reading of the same game by the other judge: how EXPECTED each move was from a player of that rating, rather than how good. Its own opt-in section, off by default, and it names the moves the engine ranked first that the human model did not see coming. Batch review answers the question one game cannot. Every game in the box is analysed against one engine load, and each player's numbers are pooled across all of them. THE BUG THIS FOUND, which the batch refactor introduced: both engines used the same offscreen client id. The host keys everything on it, so starting the human model DISPOSED the analysis engine and answered in its place -- every eval after that was a depth-1 human-likelihood score. It read as a sawtooth on the eval graph and as a flawless game in the numbers. Each engine has its own id now. review.js: the estimate, human-likeness and batch sections; openings read from the bundled table; one client id per engine; depth 16 / 1000ms defaults; the export carries the PGN review-core.js: estimate() and its weights review.html: two toggles, two sections, two table-of-contents entries review.css: the estimate block, the pooled tables, the PGN block i18n/locales: four strings and two tooltips Type: improvement Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lockout was written when the host took a time budget and nothing else. The host takes a depth now, and one that predates the field falls back to the time cap, so there is no combination left that cannot run -- but the option stayed disabled and still said "needs a time budget". A missing host is the only reason an engine is unselectable now. An earlier pass thought it had removed this. It replaced a block that had already moved, without asserting the anchor matched, so the edit silently did nothing and the old text shipped. The ladder pins the rule now rather than the edit. review.js: updateEngineOptions derives disabled from the host alone Type: bug fix Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Confirmed in real games from every colour, which was the one thing the shape matcher could not be sure of. Chess.com rotates the BOARD per player but draws the promotion panel in screen orientation, so index 0 is the top-left cell whoever is promoting and one reading order serves all four seats. content-script.js: the note above FOURPC_PROMO_ORDER says confirmed, not assumed README.md: the same, where it describes the picker Type: docs Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README.md: Game review rewritten for the estimate, human likeness, batch runs, offline openings, phases, titles and the export; a Game Review row in the settings reference; Copy Diagnostics carries the worker cold starts Type: docs Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
manifest.json: 3.1.217 -> 3.1.218 Type: tooling Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lichess put the opening explorer behind OAuth: every explorer route answers 401 at its proxy while the tablebase, which declares `security: []`, still answers anonymously. Their own site gets through on a session cookie, but the explorer answers `access-control-allow-origin: *` to every origin except lichess.org's, and a wildcard forbids credentials -- so no extension can ever use that route. A per-user token is the only mechanism left open. It is a credential and is handled as one: a password field, dropped from an exported settings file, and present in the diagnostics report only as "set" or "not set". Both of those files exist to be sent to other people. A 401 now names the setting that fixes it instead of reading as a dead endpoint. Not verified end to end: I have no token and did not make one, so that a VALID token is accepted is inferred from lichess's own API spec, not tested. The header is confirmed to be sent, and rejected as expected with a fake one. background-script.js: lichessAuthHeader on the explorer fetch, a 401 that names the fix, "token set" in the report general.html / general.js: the field, and its registration SettingsPage.js: an export never carries it options.css: .set-secret README.md: the setting, and what it is for i18n/locales: two strings Type: bug fix Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
manifest.json: 3.1.218 -> 3.1.219 Type: tooling Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
They dispatched only 'change', and FormElement binds 'input' for a number field and 'change' only for checkbox/select -- so every stepper on that page moved the number on screen and stored nothing. Leave the page and it had reverted. Typing into the same field always worked, which is exactly what hid it: the value was there until you navigated away. Affects every number on the page: Elo, Search Time, Fallback Poll Interval, and all four Simulated Think/Move fields. Confirmed on Search Time and Elo before and after -- the + button showed 350 with nothing stored, and now stores 350. 'change' is still dispatched, because the humanize mix rows listen for it. general.js: initSteppers dispatches 'input' as well, and its comment no longer says the wrong thing Type: bug fix Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 300ms pause before a database move's first click is the one number that has to be tuned per machine: it exists because a known solution has no search in front of it to let the board finish animating, and how long that takes depends on the page and the machine. Raise it if moves are being dropped as mismatches, lower it to solve faster, zero to disable. Read fresh per move rather than from the config snapshot, so it can be dragged while watching puzzles solve. An unset, junk or out-of-range value falls back to the 300 it has always been. popup.js: puzzle_move_delay_ms(), and the constant is now the documented default general.html: the row, with the same steppers every other number has i18n/locales: the label and its tooltip Type: new idea Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
manifest.json: 3.1.219 -> 3.1.220 Type: tooling Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mephisto 3.x — a full revival of the 2.x base. Every site adapter was re-anchored against 2026 DOMs, the engine lineup was replaced, and detection moved from polling to events. Released builds: https://github.com/IchNukeDichWeg/Mephisto/releases
UCI_Elorangecan be set by hand when that read is wrong
that endpoint behind OAuth, so there is a field for a personal API token: their own site
passes on a session cookie, but the explorer answers
access-control-allow-origin: *toevery origin except lichess.org's, and a wildcard forbids credentials, so no extension can
use that route. The token is a password field, is dropped from an exported settings file,
and appears in the diagnostics report only as set/not-set
player's public Chess.com archive. Every position searched once, so before and after come from
the same search; Lichess accuracy and the panel's own move-quality bands; alternate lines; an
eval graph with the opening/middlegame/endgame boundaries; think time from the clock comments;
openings named offline from a bundled CC0 table keyed by position, so transpositions name
correctly; an optional Maia pass reporting where a move sat in the human model's own ranking,
and a human-likeness report built on it; batch review pooling a player across every game in the
file against one engine load; fair-play indicators with an overall estimate that says what it is
worth, presented as measurements and never a verdict; and an export that clones the page with
its styles inlined into one self-contained file
accuracy and average win-drop shown as you tune it
separately from clock mode, which paces the search
position is never answered from the other site's records
front of it, so nothing else lets the board finish animating before it is clicked
web_accessible_resources; panel assets are streamed from the workerextension's own folder in place, so the extension id — and every native-messaging
registration made against it — survives
version that disagrees with the release or a missing file means nothing is written at all
the panel says once what changed
why the last move was or was not played and the recent trace on the clipboard, with nothing
identifying in it
though the worker that was slow is long gone
+/-buttons on the settings page dispatched onlychange, while the form layerpersists a number field on
input— so every stepper moved the value on screen and storednothing, and typing into the same field always worked, which is what hid it
every bug report named a build that was not running; it reads the manifest now
Supersedes #33, which carries the earlier discussion (including @IazLur's anti-cheat notes and #34).