Get faster at writing the code you already understand.
Reading a solution teaches you the idea. It does not teach your hands the syntax, and in an interview it is your hands that are being watched. Code Trainer is a VS Code extension that makes you type every solution out — no pasting, no autocomplete unless you ask for it — and measures how fast and how accurately you do it.
Install it in one command:
curl -fsSL https://raw.githubusercontent.com/jairoFernandez/code-coach/main/scripts/install.sh | bashThen reload the window and open the Code Trainer icon in the activity bar. Not on the Marketplace — see Installing it for what the script does and how to pin a version.
It ships with a catalogue of interview problems, generates fresh ones with a local Ollama model, and tracks which topics you keep failing so the next exercise targets them.
| Mode | Ghost text | IntelliSense | Paste | What it trains |
|---|---|---|---|---|
| Typing target | the full solution | off | off | raw speed and accuracy |
| Assisted | AI, after a 900ms pause | after a 700ms pause, costs points | off | fluency with help |
| TDD | off | off | off | test-first discipline |
| Blind | off | off | off | pure recall |
In typing target the reference solution is shown as dimmed ghost text and only the correct character advances — a wrong key is swallowed and counted against accuracy. In TDD the failing tests open first, a watcher starts in the terminal, and you write the implementation with no assistance at all.
Paste is blocked in every mode. That's the point.
The Code Trainer icon in the activity bar opens the catalogue, grouped by
interview pattern — the unit you actually study for interviews. Difficulty
is shown per problem (E/M/H), and the pattern header tracks your progress.
Patterns, in study order: arrays & hashing, two pointers, sliding window, stack, binary search, linked list, trees, heap, backtracking, graphs, dynamic programming, intervals, greedy, design.
It ships with 13 hand-written problems. Clicking one asks for the language and mode, then starts a session.
Three ways, under Add problems in the sidebar:
| Command | What it does |
|---|---|
| Generate with the model | Pick a pattern, difficulty and count; the model writes them |
| From a statement | Paste a problem you already have; the model writes the tests and solution around it |
| Import / export | Load or share a JSON pack — the format is documented in the command |
From a statement is two steps. The first run opens a scratch document; write or paste the statement there, then run it again (or use Code Trainer: Add Problem From a Statement from the palette) to build the exercise. A modal prompt would have sat on top of the editor, leaving nowhere to type.
Export covers your own problems, the bundled catalogue, or both — the bundled ones are the best worked example of the pack format if you want to write your own.
Retries are repairs, not redos. When an attempt fails, the model is shown
its own output and the exact complaint — a missing field, or the failing test
output — and asked to correct it. Re-asking the original question just
reproduces the same mistake. Measured: gpt-oss:20b recovered three missing
fields from a broken reply in 29s.
Watch it work. Generation opens the Code Trainer output channel and logs every step: when the model starts responding, how long the reply took, each test run per language with its verdict, and the failing output when something is rejected. A run that takes minutes is then legible rather than a spinner.
Everything is verified by running it. A generated or imported problem is materialised to a scratch directory and its tests are executed twice: against the reference solution (must pass) and against the starter (must fail). Anything else is rejected, and generation retries up to three times before giving up.
That two-sided check is the point. A model will happily emit tests that don't
compile, assert the wrong answer, or pass against an empty function — none of
which any static check catches. Measured on a real run: qwen2.5-coder:0.5b
produced an unusable problem in 2.8s and was rejected; gpt-oss:20b
produced a working one in 54s and was accepted. Use a model of at least 7B
or expect to wait through retries.
Only the languages that verify are saved, so a problem never ships with one working implementation and one broken one.
The Ask view below the catalogue is a chat with the local model. It is given the problem statement, the reference solution and the code you have actually written, so it can answer "what's wrong with my approach?" concretely. It is told to hint rather than hand over the answer unless you ask outright.
The conversation has two surfaces:
- The sidebar lists your conversations, most recent first, with a preview and a relative timestamp. Clicking one opens it in the panel and marks the row selected — it does not expand inline, since the panel is already showing those messages. Each conversation can be deleted on its own. There is no input box here: a textarea and a send button cost more vertical space than the sidebar has.
- The panel (
Ask the Coach, or the button in the sidebar) is where you type. It opens beside the code, reads top-to-bottom, and has room for code blocks.
Both read the same history from the host, so a reply asked for in the panel appears in the sidebar as it arrives — including the "thinking…" state.
Conversations are stored in the framework's mini-ORM (src/models/ChatTurn.ts,
global provider). Each has its own identity and is labelled by the problem
it was started on, so one problem can hold as many conversations as you like —
New conversation always starts a fresh one. Questions asked with no exercise
open go to a "General questions" thread. They survive a window reload, and
Clear only empties the one in view.
Opening a past conversation pins it until you press ← Current or start a new exercise, so browsing history never redirects a question to the wrong thread.
The conversation resets when you start a new exercise.
| Command | Keybinding |
|---|---|
| Code Trainer: Start Practice Session | cmd+alt+p |
| Code Trainer: Run Exercise Tests | cmd+alt+t |
| Code Trainer: Debug Exercise Tests | cmd+alt+d |
| Code Trainer: Finish Practice Session | — |
| Code Trainer: Open Dashboard | — |
| Code Trainer: Settings | — |
| (catalogue) Open Problem | click |
The status bar shows live WPM, accuracy and progress, and turns amber when accuracy drops below 85%. Click it for the action menu. Between sessions it shows your day streak instead, and turns amber when a streak you already have has not been fed today.
The dashboard's Habit card is about consistency rather than speed: your current and best day streak, how many days you have practised in total, and the last 30 days as a calendar.
A day counts when you finish a session — passing is not required. A day spent losing to a hard problem is still a day of practice, and breaking your streak over it would be exactly the wrong incentive. Days are your local calendar days, so a late-night session and an early-morning one are two days.
The daily goal is problems or minutes, and either one satisfies it: some days are one hard problem over half an hour, others are six quick drills. Edit it with Edit goal on the card; set a half to zero to ignore it. The default is 1 problem or 15 minutes.
The streak also appears above the problem list in the sidebar, and an AI agent
can read it through get_practice_habits.
Debug Exercise Tests (cmd+alt+d, or the Debug button in the
dashboard) runs the exercise's tests under the debugger: set breakpoints in
your file, and the tests drive it. Debug runs are counted in the session
metrics but never scored against — stepping through your own attempt is
learning, not cheating.
TypeScript and JavaScript debug through Bun's debug adapter (the tests import
bun:test, so Node cannot run them) and need the Bun for Visual Studio
Code extension — the command offers to install it. Python uses the Python
extension's debugpy running pytest.
Not available in typing target: mid-transcription the file does not compile yet, so there is nothing to debug.
TypeScript, JavaScript (bun test) and Python (pytest). Adding a language
means one entry in src/lib/languages.ts — nothing else branches on language.
Exercises, ghost text and post-session coaching all come from a local model. Configure it in settings:
Changing the model. Run Code Trainer: Settings — from the palette, the
sidebar, or the status bar menu — and pick from the models actually installed
on your endpoint. The chat header has the same dropdown. The endpoint URL and
everything else is editable there too, or in the Settings UI under
codetrainer.llm.
Model names need their tag. Ollama treats qwen2.5-coder and
qwen2.5-coder:0.5b as different models, and asking for the untagged name
404s. Leave the setting empty to auto-select, or pick from the dropdown in the
dashboard — it lists only models you have actually pulled. If the configured
model is missing, the client resolves it to an installed one and the dashboard
dot turns amber.
Use a coding model of reasonable size. A 0.5b model returns JSON whose code
fields are nonsense; the generator validates the output and falls back to the
built-in exercise bank rather than putting junk in your editor, but you'll get
much better exercises from something like qwen2.5-coder:7b and up.
If Ollama is unreachable everything still works — you get the built-in exercises, no ghost text, and no coaching.
Statements and hints can be shown in English or Spanish — pick with
Problem language in the sidebar, or codetrainer.problemLanguage.
Only prose is translated. Function names, tests and reference solutions stay in English: translating them would break the test runner, and interviews are conducted against English identifiers anyway.
Problems ship in English. Translate missing problems uses the model to fill in the rest; translations are stored separately from the problems, so a bundled problem can be translated without being duplicated.
Connect an AI agent in the sidebar gives you a one-line command to wire Claude Code — or Codex, Kimi, anything speaking MCP — into your catalogue and practice data.
| Tool | Purpose |
|---|---|
get_practice_stats |
Pass rate and accuracy per pattern, what you've never attempted, recent sessions |
get_practice_habits |
Day streak, progress against your daily goal, per-day activity |
list_problems |
The whole catalogue, filterable by pattern, difficulty or language |
get_problem |
One problem in full — statement, starter, solution, tests |
create_problem |
Add a problem; its tests are run before it is saved |
get_storage_info |
Where the data lives, and whether anything is recorded |
Worth asking it:
I have an interview in two weeks. Look at my stats, work out what's missing, and build a day-by-day plan — creating any problems I don't have yet.
A capable agent writes far better problems than a local 7B model, and the verification is identical: tests must pass against the solution and fail against the starter, or nothing is saved. Your catalogue cannot be filled with broken exercises, whoever wrote them.
How it works. The server is a separate process that reads and writes the same JSON files the extension's ORM uses — no port, no daemon, no sync. It cannot drive a live practice session; that stays in the editor, which is where typing belongs. Problems it adds appear after a window reload.
Configure it once. The path you are given is in VS Code's global storage,
not inside the extension folder — extension directories carry the version
(…/jairoFernandez.code-trainer-0.0.1/) and are replaced on every update,
which would silently break the config. The extension refreshes the copy at the
stable path each time it starts.
Every finished session is recorded (in globalState, so it follows you across
workspaces). The dashboard shows average and best WPM, accuracy, pass rate, a
WPM trend sparkline, and a weakest topics table ranked by pass rate then
accuracy. Those weak topics are fed straight back into the next exercise
generation.
bun run test # 375 tests
bun run test:coverage # with a coverage report
bun run test:watchTests live in test/unit. bun run package runs them first, so a broken
build cannot be published.
Coverage sits around 77% of functions / 84% of lines, and is reported
rather than enforced: src/mcp/store.ts is exercised by spawning the built MCP
server as a subprocess, which the instrumenter cannot observe, so a threshold
would fail every run instead of catching a regression.
test/helpers/ holds copies of pure functions lifted out of src — importing
the real module would drag in vscode. helper-drift.test.ts pins each copy to
its original, so a copy cannot silently go stale.
The extension is not on the Marketplace. Each tagged release publishes a
.vsix to GitHub Releases,
and this installs the latest one:
curl -fsSL https://raw.githubusercontent.com/jairoFernandez/code-coach/main/scripts/install.sh | bashIt finds your editor CLI (code, cursor, codium, …), downloads the .vsix,
checks it against the published SHA-256, and installs it. To pin a version or
choose the editor, pass options through:
curl -fsSL https://raw.githubusercontent.com/jairoFernandez/code-coach/main/scripts/install.sh | bash -s -- --version 0.1.2 --editor cursorThen reload the window — Cmd+Shift+P → Developer: Reload Window.
Prefer not to pipe a script into a shell? Download the .vsix from the
releases page and install it directly:
code --install-extension code-trainer-0.1.2.vsixTo build from source and run in your own editor rather than a development host:
make install # test, build, package, and install the .vsixThen reload the window — Cmd+Shift+P → Developer: Reload Window.
make on its own lists every target. The ones you are likely to want:
| Target | What it does |
|---|---|
make install |
Build a production .vsix and install it |
make install-release |
Install the latest published release instead of building |
make reinstall |
Remove the installed copy first — use after changing the id or contributions |
make uninstall |
Remove it from your editor |
make package |
Build the .vsix without installing |
make run |
Open a development host instead of installing |
make clean |
Delete dist/ and any packaged .vsix |
make package skips the rebuild when nothing under src/ has changed. For a
fork of VS Code, pass its CLI: make install CODE=cursor.
Every push and pull request runs CI: typecheck, unit tests, a production build
and a vsce package, with the resulting .vsix kept as a build artefact.
Cutting a release is a tag:
git tag v0.1.3 && git push origin v0.1.3That builds, packages and publishes the .vsix and its SHA-256 to GitHub
Releases, which is what scripts/install.sh installs from. The Release
workflow can also be run by hand from the Actions tab with a version — it
creates the tag itself in that case.
The tag is what decides the shipped version; package.json keeps its plain
numeric version in git, and the release stamps the tag's version in at build
time (see scripts/stamp-version.ts).
bun install
bun run dev # watch both the extension and the webview, then press F5
bun run launch # or: build once and open an Extension Development Host
bun run package # produce a .vsix
bun run devonly builds — F5 (orbun run launch) is what opens the dev host. Reload the dev host withCmd+Rafter a change.
Built on vsceasy. The editor-surface primitives it uses —
defineCompletion, defineInlineCompletion, defineTypingGuard,
defineDecoration, defineTerminal and the LLM client — were added to the
framework for this project; see its docs/guides/editor-surface.md.
src/
├── completions/hints.ts # non-invasive IntelliSense (delay + gate)
├── inlineCompletions/predict.ts # Ollama-backed ghost text
├── typingGuards/practice.ts # enforces the practice mode, blocks paste
├── decorations/target.ts # renders the typing target
├── terminals/runner.ts # test runner
├── statusBars/wpm.ts # live scoreboard
├── commands/ # start / run tests / finish
├── panels/dashboard.ts # dashboard RPC
├── stores/session.ts # the live session + metrics engine
└── lib/ # languages, exercises, generator, history, runner
{ "codetrainer.llm.provider": "ollama", // or "openai" "codetrainer.llm.baseUrl": "http://localhost:11434", "codetrainer.llm.model": "", // empty = auto-select an installed model "codetrainer.llm.apiKey": "" // only for remote endpoints }