Kata Studio

A workbench for the Codewars kata trainer: an AI tutor that hints instead of solving, in-place kata translation, real formatters for C, C++, Python, Kotlin and Rust, a trainer laid out for the code rather than the chrome, and a practice history on the dashboard.

Detta är versioner av skriptet där koden uppdaterats. Visa alla versioner.

  • v1.8.0 2026-08-28

    feat: collapse the description column and colour brackets by depth

    Instructions and Output are two tabs over one panel, so one toggle in Codewars' own tab bar collapses both, cutting the column to a strip and giving the width to the editors. The state persists without a reload.

    Brackets are coloured by nesting depth, six levels, with an unmatched closer underlined. The colouring reads getLineTokens() rather than an overlay: an overlay sees the raw line and would colour a brace inside a string and shift the depth of everything after it.

    RESET now offers the last test run alongside Codewars' original stub. Alt+Shift+R was the only way back to that checkpoint, and a shortcut nobody is told about is not a way back; the button is where a reader already goes to undo. With no checkpoint, the click is left alone.

    CodeMirror's folded-code marker — a blue arrow under a purple glow, on a line squashed to a third of its height — is replaced with a quiet marker in the editor's own colours.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01CimNYeMUxWFcKfXqSawZaF

    feat: fold the sample tests to their own header

    They are read once and then in the way for the rest of the kata. The header row is the toggle, as the tab bar is for the description column.

    Hiding the fixture alone leaves its space behind: Codewars sizes the two editors as 60% / 40% of the column and writes the inner height in px from its own JS, which does not recompute on a resize event. Collapsed, the column becomes a flex stack and the solution takes what is left over the fixture's header.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01CimNYeMUxWFcKfXqSawZaF

    feat: dock the tutor panel whenever the editor column can take it

    The panel reserved space only above 1500px and overlaid below that, on the reasoning that a narrower window has nothing to spare. Overlaying is not merely cramped: the panel sits on top of TEST and ATTEMPT, so the tutor had to be closed before every run.

    That reasoning also predates the description column, which now folds and hands back most of a column. So the width is measured on #editors_area after each class change instead of assumed from the viewport: dock, fold the description if the column is under 720px, and overlay only if it is still under 480px. Closing the panel unfolds what it folded; a column the reader folded themselves stays folded.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01CimNYeMUxWFcKfXqSawZaF

    feat: give both tabs the column's height and one rule for docking

    The description card is sized to the column rather than to its content: a short kata fills it instead of leaving a card that ends early, a long one scrolls inside it, and the console — whose own h-full had no height to be full of — stops being a 154px box with a grey band under it.

    Docking drops the ladder. It reserves the space, measures the solution editor, and gives the space back if that came out under 600px; the question is asked again on every layout change, so folding either pane brings the panel alongside the page. The description is never folded on the panel's behalf.

    Two fixes found while measuring. Codewars writes the solution editor's height into .text-editor as inline px, measured once: a page loading with the sample tests folded got a number too tall for the unfolded layout, and the editor overflowed its container on unfolding. sizeSolutionEditor() restates it by Codewars' own formula. And CodeMirror is refreshed in the same frame as the layout change instead of 240ms later, which read as the editor redrawing itself after the click.

    The sample tests' chevron moves to the left of the label, where a header that folds says so at the point the eye is already on.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01CimNYeMUxWFcKfXqSawZaF

    feat: lay the editor column out once and open the tutor when it fits

    The panel opens itself whenever docking leaves the solution editor wide enough, and nothing is recorded about who opened or closed it: the question is asked again on every layout change, so folding a pane is what brings the tutor alongside the page.

    The column had three mechanisms disagreeing about its height: percentage heights on the two blocks, the editor's own height written into .text-editor as inline px from JS and measured once, and 60px of padding underneath that Codewars' layout overran into — which is why TEST and ATTEMPT sat half under the bottom of the window, and why folding the sample tests left a gap where that padding was. One flex column states it instead: the block headers take what they need, the editors take the rest, and .text-editor is height:100% with no number to keep in step. Solution, Sample Tests and the buttons now stand 8px apart rather than 56px, which was three separate paddings and a margin.

    --cw-title-h kept a height measured before the title's stats row wrapped: Codewars replaces .game-title, and the observer was left on the node it replaced. It is re-observed, and republished on every layout change.

    The rainbow brackets were repainting forever: markText re-renders the lines it touches, that fires viewportChange, and viewportChange scheduled the next repaint. A repaint now needs the document or the viewport to have actually moved, and an edit with no bracket in it is ignored — marks travel with the text on their own.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01CimNYeMUxWFcKfXqSawZaF

    fix: retract the tutor when the room goes, and clear marks in one operation

    Losing the room is not a reason to float over the code: the panel goes back the way it came. Only the reader's own click brings it out over the page, which is the one call that does not retract it.

    Rainbow brackets cleared the previous marks outside cm.operation(), so each of several hundred clear() calls re-rendered the document on its own. One repaint of a 300-line bracket-heavy file measured 4.5 seconds; inside the operation it is 45ms, and the buffer around the viewport drops from 20 lines to 5.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01CimNYeMUxWFcKfXqSawZaF

    fix: keep an overlay the reader asked for

    A panel that shows itself goes away when the room does, but one summoned by hand was going with it — a fold anywhere else closed the tutor the reader had just opened to read.

    panelSummoned is the one thing about the panel that is remembered rather than measured. It is set by the ways in that are the reader's own — the edge tab, Alt+A, the swipe, an Ask chip, a question, an error worth reading — dropped the moment the panel docks, and not persisted: after a reload the layout decides again.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01CimNYeMUxWFcKfXqSawZaF

    feat: keep the tutor's conversation and offer it back per kata

    Codewars keeps nothing of this and the panel's log node does not survive a route change, so an exchange ended when the reader left the kata — often mid-hint, if they went to look something up. It is written to GM storage keyed the way drafts are, and restored when they come back: ten kata, fourteen days, forty turns each.

    Two arrays are stored. history is what the model was sent, where a user turn also carries the editor, the run output and the attachments; view is what the log showed, which is the question alone. The frozen kata brief goes with them, because rebuilding it on return would move the prefix the provider's prompt cache is keyed on.

    Two fixes fell out of it. An aborted request still settles, and its handlers pushed the abandoned answer into whatever conversation had replaced it — pressing "new conversation" mid-reply left the new one opening with the old one's last words; chat.epoch drops those writes. And a docked panel now hides its close control: it is a pane of the layout and comes back on the next fold however it is dismissed.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01CimNYeMUxWFcKfXqSawZaF

  • v1.7.0 2026-08-28

    feat: keep the editor's draft and offer the last tested version back

    Codewars writes the editor to localStorage as you type, then sets that value to null on the next load and fills the editor from the server, which only has what TEST or ATTEMPT last sent. A refresh therefore loses everything typed since the last run, and the site's own copy cannot be borrowed: it is cleared before anything could read it.

    The script now keeps its own, under prettier-codewars:drafts, and puts it back on the next visit. What makes that safe is the baseline stored with it: the buffer as it stood before the learner's first real edit. Codewars' own setValue, the format that runs on entering a kata, and the restore itself all move that baseline; typing does not. The draft goes back only over a buffer still identical to it, so a solution submitted from another machine is never overwritten. The restore is re-applied for six seconds because Codewars can fill the editor after we do, and stops the moment the learner types.

    TEST, ATTEMPT and SUBMIT also record a checkpoint, and Alt+Shift+R goes back to it. Replacing the buffer is the one thing here that can lose work, so it is the one thing that asks first; the confirm dialog reuses the settings dialog's styling, now keyed on a class rather than on that dialog's id.

    The README said Ctrl+S took nothing away because the trainer had nothing to save. The first half holds, the reason does not.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_013kMb42419UbjmuTMz6F8GW

  • v1.6.0 2026-08-28

    feat: translate whole sentences and stop rewriting the prompt prefix

    Description translation segmented by text node, so inline markup cut a sentence into pieces that were each translated alone. On Valid Braces that left "This Kata is similar to" in English in front of a Chinese clause: a model handed a third of a sentence has no way to move it around the link, which is what the target language needs. Segments are now maximal runs of text and inline elements, with each inline element standing in as {{n}} and spliced back where the translation put it. Inline elements are walked afterwards, so a link's own label is still translated, and a reply that lost a placeholder keeps its original. Nothing is wrapped: a span would sit between Codewars' p > code rules and the code they style.

    The AI request rebuilt the kata context, including the editor buffer, ahead of the whole conversation, so one keystroke moved every token after the system prompt and no turn could hit the provider's prompt cache. The prefix is now the tutor prompt, a kata brief built once, and a history that is only appended to; the editor, the run output and the community solutions ride on the newest turn and are left out when they are the same text an earlier turn carried.

    Drop the C autocomplete. Its candidate cache was keyed on the whole buffer, so every keystroke re-scanned the document, and it offered no ranking to pay for that.

    Ctrl+S takes nothing away, which the README now says with its reason: the trainer writes its draft to localStorage on every edit, and Codewars' own binding is CodeMirror's save command pointed at validate().

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_013kMb42419UbjmuTMz6F8GW

  • v1.5.0 2026-08-26

    feat: format the buffer on Ctrl+S instead of running the tests

    Ctrl+S is the format-on-save reflex from every other editor, and on the Codewars trainer it starts a test run instead. There is nothing to save here, so the key is now the formatter's.

    The listener sits on document in the capture phase, next to the one that formats before TEST/ATTEMPT/SUBMIT. A CodeMirror keymap entry does not work: it fires only while the editor holds focus, and CodeMirror stops propagation after its own handler, by which point a capture-phase listener elsewhere on the page has already run. preventDefault also keeps the browser's "save page" dialog away, which would otherwise block every later keystroke.

    Ctrl+Shift+S and the plain key are left alone, and the handler is gated on the trainer being active, so routing out of the trainer hands Ctrl+S back to the page.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01SJhfQKUXwvhyXofuaSGUWm

    feat: format five languages with their own real formatters

    The hand-written C formatter is gone. In its place each language is formatted by the tool its own community uses, compiled to wasm and fetched at first use: clang-format for C and C++, Ruff for Python, ktfmt for Kotlin, rustfmt for Rust. That is a thousand lines of lexer removed and four languages gained, and it settles the limitation the old one was built around -- C++ was excluded because a lexical formatter cannot tell a < b from map<int, vector<int>>, and clang-format is a real parser.

    Versions are pinned permanently rather than tracked. The loader reaches past every package's public entry point, because none of them was built for a userscript, and a version bump is exactly what would break that. Two of the four ship for Node: their brotli artifact is fetched and expanded here, then handed over with init({bytes}), since Chrome still has no DecompressionStream('brotli').

    injectImportMap() runs site-wide at the bottom of the IIFE. The Kotlin and Rust packages import bare specifiers from inside their own modules, and an import map only applies to graphs that start loading after it lands; injecting it lazily at first format was tried and does not work.

    autoFormat() stays synchronous although loading is not, because the handler on TEST/ATTEMPT/SUBMIT runs in the capture phase and cannot hold a click open for a 2 MB download. The dashboard prefetches the binaries for the languages the reader has a rank in, so the trainer usually finds them cached; one binary covers C and C++ together.

    The formatter test suite is removed with the formatter it tested -- asserting what clang-format prints is testing clang-format. What the harnesses cover now is the wiring: a formatter that cannot load leaves the buffer untouched, the import map is present and pinned, and the prefetch fetches one binary per ranked language and none for the rest.

    Verified in the browser on all five languages.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01SJhfQKUXwvhyXofuaSGUWm

    feat: let the tutor teach at length, and read the top solutions

    Two changes to the same idea: the kata is the vehicle, the language is what the learner keeps.

    The prompt used to cap every reply at two to five sentences. That cap was aimed at not leaking the answer, but it also made the tutor terse about things that have nothing to do with this kata -- how a feature behaves, what a library call guarantees, why an algorithm costs what it does. The prompt now splits its content in two and rations only the half that moves the learner toward the answer. The other half is explicitly unrationed, because being brief about general knowledge is not caution, it is a worse answer.

    The obvious failure mode of that instruction is a model that opens every reply with a paragraph of background, which a reader learns to skip. The prompt says not to, twice and in different words: a narrow question gets a narrow answer, and the wider point is reached for when the code opens onto one, never on a schedule and never as a preamble.

    A new panel button fetches the kata's three highest-voted community solutions and turns the conversation to what they do differently. Both of Codewars' vote axes are carried through separately rather than summed: best-practices and clever disagree constantly, and the cleverest solution to a kata is regularly not the one worth imitating -- the prompt turns on exactly that distinction.

    Codewars withholds solutions until the kata is solved, and unlocking them early forfeits its honor. The script only ever GETs that page and never touches the unlock control; on an unsolved kata it says so and stops rather than routing around it. A test asserts the method.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01SJhfQKUXwvhyXofuaSGUWm

    feat: release 1.5.0

    Formatters for C, C++, Python, Kotlin and Rust in place of the hand-written C one; Ctrl+S formats instead of running the tests; the tutor answers general questions at length and can read the kata's top-voted community solutions.

    Readers get a behaviour change worth naming: formatting is now whatever the language's own tool produces, long lines included, and the first format in a language fetches a wasm binary between 0.5 and 1.3 MB from jsDelivr.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01SJhfQKUXwvhyXofuaSGUWm

  • v1.4.0 2026-08-25

    feat: add a practice history to the dashboard

    Codewars keeps no history outside the completed list on a profile, and the dashboard is a suggested kata, two promotions and a forum feed. A second gate, onDashboardPage(), now puts a card above the suggested kata: a timeline grouped by day with the languages each kata was solved in and a link back into the trainer, a bar of the last thirty days, and the current streak.

    The completions come from the public API rather than from any page, cached in GM storage for ten minutes and painted from cache before the refresh lands, so arriving on the dashboard does not redraw it. The username comes from the header's profile link; there is no /me endpoint.

    Two things this needed from existing code. removeAds() only marks nodes, and the rule acting on the mark lives in the trainer sheet, so the dashboard sheet repeats it. nearestAdContainer() climbs to div[class*='w-full'], which on the dashboard is the whole main column: one house ad hid the entire page, so isProtected() now refuses any node containing the page's own content.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01WcD1hYpBTvWD3oNZy23hzk

    feat: rework the history card around a year calendar

    The first cut of the card was a thirty-day bar, a refresh button and rows carrying only a name. Four changes, all of them things the card is read for rather than things it does.

    The bar becomes a GitHub-style calendar: one column per week over a year, week-aligned so every column is a full Sunday-to-Saturday, month labels along the top, and the days after today drawn as holes rather than as zeroes so the current week does not read as a lapse.

    Rows now carry the kata's rank and a Train button. Ranks are not in the completions payload, so they come from the kata endpoint through a pool of three and are cached for good; the list renders first and the badges fill in. Rank colours and language chips reuse Codewars' own rank colours and icon font, with a text fallback for languages the font has no glyph for.

    The allies box and the forum feed are hidden outright instead of folded: a fold is still a row to skip past. The refresh button is gone; the card already refreshes itself on a TTL.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01WcD1hYpBTvWD3oNZy23hzk

    feat: make the history calendar a full-width month

    The year graph left most of the card empty and asked to be read at a glance it could not support. It becomes one month, in the site's own colours, filling the card.

    The grid is transposed for that to be possible: weekdays are columns and weeks are rows. A month is five weeks, and five square columns spanning this width would be a grid thirteen hundred pixels tall; seven columns of one fraction each fill the width at thirty pixels a row. Each cell now carries its day, and the 1st carries its month, which is the only label a single-month grid needs.

    The ramp is Codewars' own: the legacy red through the orange to the honor gold, taken from the site's custom properties rather than picked to match them.

    The card also moves below the suggested kata — what to do next is the reason to open the dashboard, the history is what you look at afterwards.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01WcD1hYpBTvWD3oNZy23hzk

    fix: shrink the calendar rows

    At thirty pixels a row the month took as much of the card as the list it sits above. Twenty is still tall enough for the day numbers.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01WcD1hYpBTvWD3oNZy23hzk

    fix: drop the day numbers from the calendar

    At sixteen pixels a row the grid is read as a shape, and thirty-five numbers inside it are noise. The date moves to the cell's tooltip, and the month the grid covers is stated once in the card header, where it does not compete with the colour.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01WcD1hYpBTvWD3oNZy23hzk

    feat: make the calendar a year-wide wall of squares

    A month of wide cells filled the card but carried a month. A year of squares carries twelve times as much and, at this width, is the same height: fifty-three columns of one fraction each land the cells near fifteen pixels.

    Two things had to be right for that. The tracks are minmax(0, 1fr) — a plain 1fr cannot shrink below its min-content, which a cell with an aspect-ratio supplies, so the wall overflowed the card. And the weekday labels now live in the same grid as the cells, filled column by column: as a separate column they stretched to the grid's height, aspect-ratio turned that height back into a width, and the cells came out half again too wide for their tracks.

    The ramp is one hue at four lightnesses — Codewars' red is hsl(10 71% 41%) — rather than red through orange to gold, which could not be put in order by eye.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01WcD1hYpBTvWD3oNZy23hzk

    feat: use Codewars' own rank badge in the history

    The rank was a bordered pill approximating the site's colours. It is now the site's own hexagon markup, so the shape, the border and the colour come from Codewars' stylesheet and follow it when they restyle. The colour arrives in an API response, so it is matched against the six known rank classes rather than interpolated into one.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01WcD1hYpBTvWD3oNZy23hzk

    fix: size the trainer panes by measurement, and follow the light theme

    Three findings from a pass over the interfaces, in both themes.

    The panes were sized against a constant strip height. The strip is the body padding plus the kata title block, and that block grows a line when the AI panel narrows the column and its stats row wraps, so the constant was six pixels short and the TEST row was clipped. A ResizeObserver publishes the measured height as --cw-title-h and the calc() uses it.

    The history card asked for --color-text, which Codewars does not define, so it fell back to the value written for the dark theme and the card read as light grey on white. The token is --color-ui-text. Both harnesses now check every custom property the sheets read against the set the site actually defines, because nothing about this is visible in the dark theme — the check found a second name on its first run.

    The calendar ramp now darkens on the light theme and lightens on the dark one, so a busier day always moves away from the page rather than towards it.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01WcD1hYpBTvWD3oNZy23hzk

    feat: add per-language stats and stop the list scrolling inside the card

    Two things the card was missing and one it should not have had.

    The header carries a chip per language: how many kata it accounts for and the rank held in it. Neither endpoint has both halves — the completions have no rank and the user resource has no per-language count — so the count is derived from the completions and the rank comes from the user resource, fetched alongside and cached with it.

    The list showed twenty-five rows through a three-hundred-pixel window, so a card on a scrolling page had a scrollbar of its own competing for the wheel. It now shows ten and links to Codewars' completed-solutions page for the rest, which has the solutions too.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01WcD1hYpBTvWD3oNZy23hzk

    feat: keep a way back to a kata you did not finish

    Codewars records that a kata was completed, never that one was opened. Press TRAIN, get pulled away, and the kata is gone: the trainer suggests a different one on every dashboard render, and nothing anywhere lists what you had started.

    Both halves of the answer already existed here. The trainer page knows which kata is open, so it writes the key, the language, the name and the rank; the dashboard already fetches the completions, so it subtracts them and shows what is left with a Resume link. Matching is per language — solving a kata in C says nothing about the Python attempt left open — and against both id and slug, because the trainer URL carries one or the other.

    This turned up a bug older than the feature: kataRank() took the first .small-hex under #app, and buildHeaderMenu() moves the reader's own rank badge into that subtree, so it returned the reader's rank rather than the kata's — in the card, and in every context block the tutor was sent.

    Co-Authored-By: Claude Opus 5 (1M context) [email protected] Claude-Session: https://claude.ai/code/session_01WcD1hYpBTvWD3oNZy23hzk

  • v1.3.1 2026-08-25 Imported from URL
  • v1.3.0 2026-08-24
  • v1.2.0 2026-05-24

    - Added K&R C AutoFormat with 4-space indentation, initial kata formatting, and Alt+Shift+F.
    - Added C-only lightweight autocomplete for local identifiers, macros, and common C standard library words.
    - Made Tab insert 4 spaces and accept the selected CodeMirror hint.

  • v1.1.0 2026-05-21

    Publish the full build with promotion cleanup, configurable Hide promotions toggle, editor polish, responsive fixes, typography, and typing effects.

  • v1.0.0 2026-05-21