Quack - Quick QACustom QA Bills for VS Code: small JavaScript or TypeScript rules that run against the active document, surface failures in a sidebar with jump-to selections, and offer one-click fixes - QA reviews you actually keep using because they live where you write. InstallOpen the Extensions view in VS Code ( Open any file, then click Check Bills in the Quack sidebar (or run Quick start
The Quack sidebarThe Quack activity-bar icon opens a sidebar split into four panels: Document, Side Effects, Bills, and Snippets. Each panel can be collapsed independently. Each panel exposes its search, sort, filter, and view-mode controls in the native title bar; click an action to open or focus the relevant in-panel UI. When a file is active, the Bills panel shows:
Bill keyboard shortcuts
The Commands
All commands are available in the Command Palette ( Missing module install promptWhen a bill's AI agent toolsQuack registers three VS Code language model tools that any LM-backed agent -- including GitHub Copilot and Claude -- can invoke when you ask it to run your QA rules. No additional configuration is required; the tools are registered automatically when the extension activates.
Example promptsThe agent decides when to invoke these tools based on context. Prompts like the following will typically trigger them:
How checks and fixes workWhen an agent calls Configuring Bills (
|
| Property | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Stable unique identifier. Used to track enabled/disabled state. |
title |
string |
Yes | Display name shown in the sidebar. |
description |
string |
Short description of what the bill checks. Shown below the title (and as a tooltip in compact mode). | |
category |
string |
Group label used to visually group bills together in the sidebar. Bills with the same category are rendered under a shared collapsible heading. Bills without a category appear in the Uncategorized group. | |
order |
number |
Sort position within the sidebar. Bills with an order value are sorted numerically and appear before bills without one. Bills without an order are sorted alphabetically by title. |
|
priority |
number \| string |
Fix order priority for Fix All. Lower values are fixed first. Accepts any integer, "Infinity", or "-Infinity". Bills without a priority are fixed last. |
|
author |
string |
Author of the bill in Full Name <email@address.com> format. |
|
repository |
string |
URL to the Git repository where this bill is maintained. | |
version |
string |
Decorative version label for the bill (semver). When set, Quack renders it as a small v<x.y.z> badge next to the bill title so users can see at a glance when a bill author has published an update. Quack does not gate bills by version. |
|
scope |
string \| string[] |
File extension(s) or glob pattern(s) this bill applies to. Extensions are plain strings such as "html", "html,htm", or ["html","htm"]. Globs are any entry containing / or *, matched against the workspace-relative file path (e.g. "src/**/*.html"). When both extensions and globs are supplied the filter is generous -- a match on either passes. When only globs are supplied the filter is specific -- the path must match at least one glob. If omitted, the bill applies to all file types. |
|
prepare |
string \| string[] |
JavaScript or TypeScript code that runs before the check phase. All preparations run in parallel. Must return an object with at least one key — the merged results are available in check as a top-level context variable. See Prepare. |
|
check |
string \| string[] |
Yes | JavaScript or TypeScript to evaluate the document. Can be inline code, an array of lines, or a path to a .js/.ts file. Supports export default. |
fix |
string \| string[] |
JavaScript or TypeScript to fix the document. Can be inline code, an array of lines, or a path to a .js/.ts file. Supports export default. |
|
granular |
boolean |
Set to true when the fix script reads this.target to operate on only the focused occurrence. Enables the Fix Selected and Ignore Selected labels when navigating between occurrences with the <</>> arrows. Without this, both buttons show their normal labels and always operate on all occurrences. |
|
browser |
string \| string[] |
JavaScript or TypeScript that runs in the browser context each time the preview page loads. Has access to window and all browser APIs. Returns the same format as check. Results are not shown in the sidebar -- browser bills run silently in the background. Can be inline code, an array of lines, or a path to a .js/.ts file. See Browser Preview. |
|
configuration |
object |
A VS Code-style settings schema declaring the bill's configurable properties. When present, a Configure icon appears on the bill card. See Bill Configuration. | |
sideEffects |
array |
Named action buttons shown in the sidebar's Side Effects section. See Side Effects. | |
analyser |
string \| string[] |
JavaScript or TypeScript that returns a flat key/value object whose entries appear as rows in the sidebar's Document panel alongside the built-in file/size/title/lang/charset rows. See Analyser scripts. | |
analyserLabels |
object |
Optional map of analyser keys to human-readable labels (e.g. { "wordCount": "Word count" }). Keys not listed fall back to the raw key name. |
|
subscribers |
array |
Scripts that react to VS Code events (file saved, editor changed, files created, ...) or a 5-field cron schedule. Each entry has a name, an event or schedule, an optional scope/conditions, and a required effect script. See Bill subscribers. |
Writing check
check runs when the user clicks Check Bills (or Quack: Check Bills). All enabled bills run in parallel.
All bill code fields (check, fix, prepare, conditions, effect) support TypeScript and export default. You can write bills in .ts files or use TypeScript syntax in inline code — Quack transpiles it automatically via esbuild. You can also use export default to return a value instead of a bare return statement.
Execution context
Inside check and fix, this is a context object with the following properties:
| Property | Value |
|---|---|
this.body |
The full document source as a raw string |
this.bill |
The bill object (id, title, description, etc.) |
this.config |
Resolved configuration for this bill — an object of key/value pairs merged from the bill's configuration defaults and the user's workspace settings |
this.filename |
Absolute path to the active document |
this.match(regex) |
Finds all regex matches in the document and returns { match, groups, line, column, length, start, end }[] (see below) |
context |
The merged object from all bills' prepare results. Empty {} if no bills define prepare. See Prepare |
progress(value) |
Report loading progress on the bill card — pass a number (0–100) for a determinate bar, true for indeterminate, or falsy to clear (see below) |
vscode |
The full VS Code extension API object — use for notifications, clipboard, commands, workspace access, etc. |
Model |
The Apple on-device AI model wrapper (macOS-only). Exposes generate, stream, classify, extract, ocr, and multi-turn createSession. See Apple on-device AI. |
this.toString() |
Returns this.body — allows this to coerce to a string in template literals and concatenation |
require(id) |
CommonJS require — fs, path, and crypto are always available; fs and path are sandboxed to the active document's directory. Other modules resolve only from the bill's own directory |
Return value
check must return one of the following:
| Return value | Result |
|---|---|
{ result: 'pass' } |
Bill passed |
{ result: 'pass', message: '...' } |
Bill passed, with a message |
{ result: 'warn', message: '...' } |
Warning: bill is highlighted amber, document is not failed |
{ result: 'fail', message: '...' } |
Bill failed: document is marked FAIL |
true |
Shorthand for pass |
false |
Shorthand for fail |
(nothing / undefined) |
Quack shows a VS Code warning alert (treat this as a bug in your bill) |
Any uncaught exception is caught by Quack and surfaced as an error result (treated as a failure).
selections
Include a selections array in the returned object to indicate exactly where in the document the problem was found. When present, a Jump To link appears in the bill's sidebar card. Clicking it applies all selections at once, so multiple occurrences can be highlighted simultaneously.
Each entry in selections is an object. You can specify positions using character indexes (start/end) or line/column coordinates — start takes priority when present. Entries can also carry a message and a type to drive grouped navigation in the sidebar (see below).
// Using line/column/length
return { result: 'fail', message: 'Missing lang attribute.', selections: [{ line: 1, column: 1 }] };
// Using character indexes (from this.match results, which include start and end)
const hits = this.match(/<font[\s>]/i);
return { result: 'fail', message: `${hits.length} <font> tags.`, selections: hits };
// Multiple occurrences with line/column
return { result: 'fail', message: '3 <font> tags found.', selections: [
{ line: 4, column: 3, length: 6 },
{ line: 9, column: 1, length: 6 },
{ line: 22, column: 7, length: 6 },
] };
Selection behaviour — character index mode:
start |
end |
Cursor behaviour |
|---|---|---|
| y | y | Selects from character start to character end |
| y | — | Places cursor at character start (no selection) |
Selection behaviour — line/column mode (used when start is not present):
line |
column |
length |
Cursor behaviour |
|---|---|---|---|
| y | — | — | Selects the entire line |
| y | — | 0 |
Places cursor at the start of the line (no selection) |
| y | y | — or 0 |
Places cursor at the column (no selection) |
| y | y | > 0 |
Selects length characters from the column |
Per-selection messages and grouped navigation:
Any selection entry can include a message string and an optional type ("warn" or "fail"). When at least one selection carries a message, the sidebar switches to grouped navigation mode: selections are grouped by their message string, and each group is displayed as a labelled block with its own prev/next navigation:
Missing alt attribute
« Select 3 »
Deprecated <font> tag
« Select 2 »
The message is coloured according to type ("fail" is red, "warn" is amber). When no selections carry a message, the sidebar shows the standard single "Select N" link.
const hits = this.match(/<img(?![^>]*\salt=)[^>]*/i);
const deprecated = this.match(/<font[\s>]/i);
return {
result: 'fail',
selections: [
...hits.map(h => ({ ...h, message: 'Missing alt attribute', type: 'fail' })),
...deprecated.map(d => ({ ...d, message: 'Deprecated <font> tag', type: 'warn' })),
],
};
canFix
Return canFix: false to suppress the Fix Issue button even when the bill has fix. This is useful when the fix is only safe under certain conditions that the check can detect:
const { parse } = require('node-html-parser');
const items = parse(this.body).querySelectorAll('td[bgcolor]');
if (items.length === 0) return { result: 'pass' };
// Only offer to auto-fix if all occurrences use the same colour
const colours = new Set(items.map(el => el.getAttribute('bgcolor')));
return {
result: 'fail',
message: `${items.length} bgcolor attribute(s) found.`,
canFix: colours.size === 1, // fix is unsafe when colours differ
};
When canFix is omitted or true, the Fix Issue button is shown normally (subject to the bill having fix and the result being fail, warn, or error).
this.match(regex)
this.match searches the document for all matches of a regular expression and returns their positions. The regex does not need to be global; this.match always finds all matches.
Returns an array of match objects:
| Property | Type | Description |
|---|---|---|
match |
string |
The full matched string |
groups |
string[] |
Captured groups (same as m.slice(1)) |
line |
number |
Line number of the match |
column |
number |
Column number of the match |
length |
number |
Character length of the match |
start |
number |
Start index of the match |
end |
number |
End index of the match |
Example: fail on all <font> tags and select every occurrence:
const hits = this.match(/<font[\s>]/i);
if (hits.length > 0) {
return {
result: 'fail',
message: `<font> tag found (${hits.length} total).`,
selections: hits,
};
}
return { result: 'pass' };
progress(value)
Report loading progress on the bill's sidebar card. A 2px accent-coloured bar appears at the bottom of the card. Useful for bills that perform slow operations (network requests, large file scans, etc.).
| Call | Effect |
|---|---|
progress(50) |
Determinate bar at 50% width |
progress(33.7) |
Accepts floats for fine-grained progress |
progress(true) |
Indeterminate animated bar |
progress(100) |
Bar fills to 100%, holds briefly, then fades out |
progress(0) / progress(false) / progress(null) |
Clears the bar immediately |
Progress is automatically cleared when the bill's result is returned. If the bar is at 100% when the result arrives, it fades out gracefully before the result is displayed.
const items = parse(this.body).querySelectorAll('img');
for (let i = 0; i < items.length; i++) {
progress((i / items.length) * 100);
// ... check each image ...
}
progress(100);
return { result: 'pass' };
require(id)
Bills can load Node modules from their own directory using require. This is useful for DOM parsing, utility libraries, or shared helper files.
require is sandboxed in two ways:
fs— all file-path arguments are validated against the active document's directory. Any path that resolves outside it throws an access-denied error.path—path.resolve()is re-rooted to the active document's directory, so relative paths resolve there rather than againstprocess.cwd().- Other modules — only modules installed in the bill's own directory can be loaded. Any resolution that escapes that directory throws an error.
// Load node-html-parser from the bill's directory (npm install node-html-parser in that directory)
const { parse } = require('node-html-parser');
const lang = parse(this.body).querySelector('html')?.getAttribute('lang');
return { result: lang ? 'pass' : 'fail', message: 'The <html> element must have a lang attribute.' };
Apple on-device AI (the Model class)
Bills running on macOS 26+ with Apple Intelligence enabled get access to the on-device foundation model (~3B parameters) via the Model global. Every call runs locally - nothing leaves the Mac, no API key, no network round-trip, no usage cost.
Model is shipped as a tiny bundled Swift binary (bin/quack-model) spawned on first use and shared across all bills.
Platform support
- macOS 26 (Tahoe) or later with Apple Silicon
- Apple Intelligence enabled in System Settings → Apple Intelligence & Siri
- On Windows, Linux, or older macOS,
Modelis still defined but every method rejects with the reasonunsupportedPlatform. UseModel.isSupportedfor a synchronous gate, orawait Model.availability()for the full status (coversappleIntelligenceNotEnabled,deviceNotEligible,modelNotReady). Model.ocr()only requires macOS -- it uses Apple's Vision framework and works even when Apple Intelligence is disabled.
if (!Model.isSupported) {
return { result: 'warn', message: 'This bill needs Apple on-device AI (macOS only).' };
}
const status = await Model.availability();
if (!status.available) {
return { result: 'warn', message: status.message };
}
Model.generate(prompt, options?)
One-shot text generation. Returns the model's reply as a string.
const summary = await Model.generate('Summarise this in one sentence:\n' + this.body, {
temperature: 0.2,
maxTokens: 80,
instructions: 'You are a concise QA assistant. Return one sentence, no preamble.'
});
return summary.includes('TODO')
? { result: 'warn', message: 'Document mentions a pending TODO: ' + summary }
: { result: 'pass' };
Options:
| Property | Type | Description |
|---|---|---|
temperature |
number |
Sampling temperature 0.0-2.0. Lower = more deterministic. |
maxTokens |
number |
Cap on tokens in the response. Omit for the framework default. |
instructions |
string |
System-style instructions that shape this call only. |
Model.stream(prompt, options?)
Streaming generation. Iterate the returned object for delta strings as they arrive, or await stream.done for the full content.
const stream = Model.stream('Write a haiku about static analysis.');
let full = '';
for await (const delta of stream) {
full += delta;
progress(true);
}
return { result: 'pass', message: full };
// Or just await the full string:
const text = await Model.stream('...').done;
Model.classify(text, labels, options?)
Pick exactly one label from a list. The model is prompted with temperature: 0 and the result is snapped to the closest case-insensitive match.
const label = await Model.classify(this.body, [ 'invoice', 'receipt', 'contract', 'other' ]);
return { result: label === 'invoice' ? 'pass' : 'fail', message: `Detected: ${ label }` };
Model.extract(text, schema, options?)
Pull a structured JSON object out of free-form text using a JSON Schema.
const data = await Model.extract(this.body, {
type: 'object',
properties: {
title: { type: 'string' },
issues: { type: 'array', items: { type: 'string' } }
},
required: [ 'title' ]
});
return data.issues?.length
? { result: 'fail', message: `${ data.issues.length } issues in "${ data.title }"` }
: { result: 'pass' };
Model.ocr(imagePath)
Runs Vision OCR on a local image file and returns any text found inside it. Uses Apple's Vision framework -- does not require Apple Intelligence, only Model.isSupported.
const path = require('path');
const docDir = path.dirname(this.filename);
const imgPath = path.resolve(docDir, 'header.png');
const { text, hasText } = await Model.ocr(imgPath);
return hasText
? { result: 'warn', message: `Image contains text: "${ text }"` }
: { result: 'pass' };
Returns an object with:
| Property | Type | Description |
|---|---|---|
text |
string |
All recognised text joined into a single string. Empty string when nothing is found. |
hasText |
boolean |
true when at least one text observation was returned by Vision. |
Multi-turn sessions
createSession opens a stateful conversation with persistent instructions and history.
const session = await Model.createSession({
instructions: 'You are a code reviewer. Reply with one short critique per turn.'
});
try {
const a = await session.send('Review this snippet: ' + this.body);
const b = await session.send('What is the most important issue from your previous critique?');
return { result: 'warn', message: b };
}
finally {
await session.close();
}
Sessions also support streaming via session.stream(prompt, options?). Always close() sessions when you're done - they hold context on the Swift side until released.
Output guarantees
- Calls are serialised inside the Swift process; you can issue many concurrent
await Model.generate(...)calls but they will be processed in order. - Every method respects the bill timeout (
quack.billTimeout). For long-running models, raise the timeout or pin atimeoutfield on a side effect. - The model is deterministic at
temperature: 0for short prompts but not byte-stable across OS updates - don't snapshot outputs in tests.
Inline example
"check": "const hits = this.match(/<html(?![^>]*\\slang=)[^>]*>/i);\nreturn { result: hits.length === 0 ? 'pass' : 'fail', message: 'The <html> element must have a lang attribute.' };"
As an alternative to escaped newlines, check and fix can be an array of strings: each item is a line of code, joined before execution:
"check": [
"const hits = this.match(/<html(?![^>]*\\slang=)[^>]*>/i);",
"return { result: hits.length === 0 ? 'pass' : 'fail', message: 'The <html> element must have a lang attribute.' };"
]
File example
Point check at a .js or .ts file in your workspace (relative path, no semicolons):
"check": "./qa/checks/lang-attr.js"
Contents of ./qa/checks/lang-attr.js:
const hits = this.match(/<html(?![^>]*\slang=)[^>]*>/i);
return {
result: hits.length === 0 ? 'pass' : 'fail',
message: 'The <html> element must have a lang attribute (e.g. lang="en").'
};
TypeScript example
"check": "./qa/checks/lang-attr.ts"
Contents of ./qa/checks/lang-attr.ts:
const hits: { line: number; column: number; length: number }[] = this.match(/<html(?![^>]*\slang=)[^>]*>/i);
export default {
result: hits.length === 0 ? 'pass' : 'fail',
message: 'The <html> element must have a lang attribute.',
selections: hits,
};
export default works in both .js and .ts files and is equivalent to a bare return.
Writing fix
fix is optional. It runs when the user clicks Fix Issue on a failed or warned bill, or when the user runs Quack: Fix All. The result is written directly back to the open document.
Quack: Fix All applies the fix for every enabled bill that has fix (and where canFix is not false), running them sequentially so each fix sees the output of the previous one.
By default, Quack re-checks bills after a fix is applied: fixing a single bill re-checks all previously checked bills (since a fix may affect other bills' results), while Fix All re-checks every enabled bill. This keeps results up to date without a manual re-check. Disable this with quack.checkAfterFix: false.
Execution context
Same as check: this is the context object, require is available, with one additional property:
| Property | Value |
|---|---|
this.target |
When the user clicks Fix Selected after navigating to a specific occurrence with the <</>> arrows, this is the SelectionTarget for that occurrence ({ start, end, line, column, length }). null when the user clicked Fix or Fix N (fix all). Use this to limit the fix to a single occurrence |
If your fix script does not check this.target, it will fix all occurrences regardless of whether the user clicked Fix or Fix Selected — which is the correct default behaviour for most bills.
To signal that your fix supports per-occurrence targeting, set "granular": true in the bill schema. This causes the sidebar to show Fix Selected and Ignore Selected (instead of Fix and Ignore) when the user has navigated to a specific occurrence. Bills without granular: true always show Fix and Ignore even in navigation mode.
Return value
fix must return the modified document string.
Example
// Inline fix: insert lang="en" if the attribute is missing
return this.body.replace(/<html(?![^>]*\slang=)/i, '<html lang="en"');
Prepare
Bills can define a prepare field — code that runs before the check phase. All preparations run in parallel, and their return values are merged into a single context object available to every check script. This is useful for expensive shared computation (e.g. DOM parsing) that multiple bills need.
Rules
preparemust return a plain object with at least one key. An empty object or non-object return is a fatal error — Quack aborts the check cycle and no bills are checked.- If two or more bills return the same key from their
prepare, a fatal error occurs (duplicate key conflict). - Preparations do not run for fixes or side effects — only for checks.
- The merged object is available in
checkas a top-levelcontextvariable (not onthis).
Execution context
prepare receives the same this context as check (this.body, this.bill, this.config, this.filename, this.match, require, vscode), but does not receive progress or context.
Example
A bill that parses the DOM once and shares it:
{
"id": "dom-parser",
"title": "DOM Parser",
"scope": "html",
"prepare": [
"const { parse } = require('node-html-parser');",
"return { dom: parse(this.body) };"
],
"check": "return true;"
}
Other bills can then use context.dom in their checks without re-parsing:
{
"id": "lang-attr",
"title": "HTML lang attribute",
"scope": "html",
"check": [
"const lang = context.dom?.querySelector('html')?.getAttribute('lang');",
"return { result: lang ? 'pass' : 'fail', message: 'Missing lang attribute.' };"
]
}
Result states
| State | Border colour | Effect on document |
|---|---|---|
pass |
Green | Bill passed. |
warn |
Amber | Concern flagged. Document is not failed. |
fail |
Red | Bill failed. Document is marked FAIL. |
error |
Red | Exception thrown in check. Treated as a failure. |
If any bill fails, the overall document status is Fail. If there are only warnings and no failures, the status is Warning. Otherwise the status is Pass.
Bill Imports (quack.billImports)
Instead of (or in addition to) defining bills inline in settings.json, you can use quack.billImports to load .json bill files by path, glob pattern, or directory. This is useful for sharing bills across projects or keeping them under version control independently.
"quack.billImports": [
"./qa/bills/lang-attr.json",
"./qa/bills/*.json",
"./qa/shared-bills",
"/absolute/path/to/bills/**/*.json"
]
Each entry can be:
- A path to a single file — loads exactly that bill.
- A glob pattern — loads all matched
.jsonfiles (e.g../qa/bills/*.json,./qa/**/*.json). - A directory path — loads all
.jsonfiles within the directory recursively.
Relative paths resolve from the workspace root. All matched bills are merged and loaded alongside any quack.bills entries. Quack watches the base directory of each pattern for changes: adding, editing, or deleting a JSON file refreshes the sidebar automatically.
Bill file format
Each .json file is a single bill. The structure is identical to an entry in quack.bills:
{
"id": "lang-attr",
"title": "HTML lang attribute",
"description": "The <html> element must declare a language.",
"check": "lang-attr.check.js",
"fix": "lang-attr.fix.js"
}
The id field is optional. If omitted, the filename without the .json extension is used as the ID (e.g. lang-attr.json -> id lang-attr).
File resolution
When check, fix, or prepare is a relative .js or .ts path in an imported bill, it resolves relative to the directory containing that bill file. This means you can keep bill logic alongside the bill definition:
qa/bills/
+-- lang-attr.json
+-- lang-attr.check.ts
+-- lang-attr.fix.ts
+-- title-tag.json
+-- title-tag.check.js
File paths in bills defined via quack.bills (in settings.json) continue to resolve relative to the workspace root.
Enabling and disabling bills
Each bill has a toggle in the sidebar. Disabled bills are skipped when running Check Bills. The enabled/disabled state is stored per-workspace in .vscode/settings.json under quack.billsDisabled and persists across sessions. This setting is not shown in the Settings UI and is managed entirely by the sidebar toggles.
Bill Configuration
Bills can declare configurable properties using a configuration object that follows VS Code's settings schema format. When a bill has configuration, a gear icon appears in the bottom-right corner of its card in the sidebar. Clicking the icon opens .vscode/settings.json (creating it if needed) and pre-populates the bill's configuration section with default values so the user can see what is available and start editing immediately.
Declaring configuration
Add a configuration object to the bill. Each key is a setting name; the value is a schema descriptor:
{
"id": "lang-attr",
"title": "HTML lang attribute",
"configuration": {
"lang": {
"type": "string",
"default": "en",
"description": "Expected value for the lang attribute on <html> (e.g. \"en\", \"fr\")."
}
},
"check": [
"const hits = this.match(/<html(?![^>]*\\slang=)[^>]*>/i);",
"if (hits.length === 0) return { result: 'pass' };",
"return { result: 'fail', message: `lang attribute missing — expected lang=\"${this.config.lang}\".` };"
],
"fix": [
"return this.body.replace(/<html(?![^>]*\\slang=)/i, `<html lang=\"${this.config.lang}\"`);"
]
}
Configuration schema properties
Each property in configuration can use the following fields:
| Field | Type | Description |
|---|---|---|
type |
string |
Data type: "string", "number", "boolean", "array", or "object" |
default |
any | Default value used when the user has not overridden the setting |
required |
boolean |
When true, the user must supply a value in quack.billConfig before the bill will run. Bills with unsatisfied required properties are greyed out and excluded from checks and fixes |
description |
string |
Short plain-text description shown in the settings file |
markdownDescription |
string |
Markdown description (use instead of description for richer text) |
enum |
any[] |
Allowed values |
enumDescriptions |
string[] |
Human-readable labels for each enum value |
minimum |
number |
Minimum value (numeric properties) |
maximum |
number |
Maximum value (numeric properties) |
Using this.config in check and fix
Configuration is available as this.config — a plain object of key/value pairs. Values are taken from the user's workspace settings if present, falling back to the schema defaults.
// this.config.lang resolves to "en" unless the user has overridden it
const hits = this.match(/<html(?![^>]*\slang=)[^>]*>/i);
if (hits.length === 0) return { result: 'pass' };
return { result: 'fail', message: `lang attribute missing — expected lang="${this.config.lang}".` };
Where values are stored
User-supplied values live in .vscode/settings.json under quack.billConfig:
{
"quack.billConfig": {
"lang-attr": {
"lang": "fr"
}
}
}
This is workspace-scoped, so each project can have its own overrides independently of others.
Side Effects
Bills can define side effects — named action buttons that appear in a dedicated collapsible "Side Effects" section above the Bills list. A bill can provide side effects alongside checks and fixes, or only side effects. The section includes a search bar, a category filter (funnel icon), and a view mode toggle (layout icon) for filtering by category and switching between list and grid views. Side effects are sorted by category, then by name.
Each side effect is an object in the bill's sideEffects array:
| Property | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Display name for the button |
icon |
string |
Codicon icon name (e.g. "file-code", "refresh") |
|
category |
string |
Category for filtering and search. If omitted, inherits the parent bill's category | |
scope |
string \| string[] |
File extension(s) or glob pattern(s) this side effect applies to. Follows the same rules as the bill-level scope field. Use "browser" to make this a browser-context side effect (see below). If omitted, the side effect appears for all file types |
|
conditions |
string \| string[] |
JavaScript or TypeScript condition code. Return true to enable the button, false to disable it. Evaluated after each check cycle. If omitted, the button is always enabled |
|
effect |
string \| string[] |
JavaScript or TypeScript code to execute when the button is clicked. If a string is returned, the document content is replaced. Supports export default. Required for non-browser side effects |
|
browser |
string \| string[] |
JavaScript or TypeScript code to run in the browser context when this side effect is triggered. Has access to window and all browser APIs. Required when scope is "browser". See Browser Preview |
|
timeout |
number |
Per-side-effect timeout in seconds. Overrides quack.billTimeout for this side effect. Set to 0 to disable the timeout entirely -- useful for long-running operations like image optimisation that should be allowed to complete in the background. If omitted, the global quack.billTimeout setting applies |
|
accept |
string \| string[] |
HTML <input type="file"> style filter. When set, clicking the button opens a file picker filtered by accept, and the user can also drag files onto the button. The selected/dropped files arrive in the effect as this.files: File[]. Accepts comma-separated extensions (".png,.jpg"), MIME globs ("image/*"), specific MIMEs ("application/pdf"), or "*" for any file. See File pickers and drops |
|
multiple |
boolean |
When true, the file picker allows selecting more than one file and the drop handler accepts multi-file drops. Otherwise only the first matching file is forwarded. Defaults to false |
Execution context
Side effect code receives the same execution context as check/fix — the this context object, require, progress, and vscode. When the side effect declares an accept attribute, this.files is populated with File objects for every selected or dropped file (always present as an array, empty when no files have arrived). See File pickers and drops for the File shape.
Return value
effect can return:
| Return value | Effect |
|---|---|
string |
Replaces the document content |
{ body: string } |
Replaces the document content |
{ selections: Array } |
Creates selections in the editor (same format as check selections) |
{ body: string, selections: Array } |
Replaces the document, then creates selections in the updated content |
| (nothing) | No document changes |
When both body and selections are returned, the body is applied first so that selection positions refer to the new content.
Example
{
"id": "html-tools",
"title": "HTML Tools",
"check": "return true;",
"sideEffects": [
{
"name": "Minify HTML",
"icon": "file-binary",
"effect": [
"return this.body.replace(/\\n\\s*/g, '');"
]
},
{
"name": "Copy to Clipboard",
"icon": "clippy",
"conditions": "return this.body.length > 0;",
"effect": [
"await vscode.env.clipboard.writeText(this.body);",
"vscode.window.showInformationMessage('Copied to clipboard!');"
]
}
]
}
The Side Effects section can be hidden entirely with quack.showSideEffects: false. If no bills define side effects, the section does not appear.
File pickers and drops
Setting accept (and optionally multiple) on a side effect turns its button into a file-input target. The two invocation paths:
- Click — opens a VS Code file picker filtered by
accept. Multi-select is enabled whenmultipleistrue. - Drag-and-drop — drop files from Finder/Explorer or VS Code's own file tree onto the button. Files are filtered against
acceptin the webview; only matching files are forwarded.
The selected/dropped files land on this.files as an array of File objects. The shape mirrors the browser File API, plus a Node-specific path so bills can hand the absolute path to native tools without re-reading:
| Member | Type | Description |
|---|---|---|
name |
string |
The file's basename (no directory) |
path |
string |
Absolute path on disk - useful for sharp, child_process, etc. |
size |
number |
File size in bytes |
type |
string |
Inferred MIME type, or '' if unknown |
lastModified |
number |
fs.stat mtimeMs |
text() |
Promise<string> |
Reads the full contents as UTF-8 |
bytes() |
Promise<Uint8Array> |
Reads the full contents as bytes |
arrayBuffer() |
Promise<ArrayBuffer> |
Reads the full contents as an ArrayBuffer |
stream() |
fs.ReadStream |
Returns a Node readable stream over the file's bytes |
The active document still populates this.body, this.filename, etc., so an effect can either work on the dropped files alone or combine them with the current document. When no document is open and the user invokes the side effect with files, this.body and this.filename are empty strings; the effect runs anyway.
accept syntax mirrors HTML <input type="file">: comma-separated entries of .ext (extension), image/* (MIME glob), application/pdf (specific MIME), or * for any file. The webview-side filter recognises extensions and MIME types; the picker dialog expands known MIME globs into VS Code filter extension lists.
{
"id": "image-tools",
"title": "Image tools",
"sideEffects": [
{
"name": "Optimise images",
"icon": "wand",
"accept": "image/*",
"multiple": true,
"effect": [
"const sharp = require('sharp');",
"for (const file of this.files) {",
" await sharp(file.path).jpeg({ quality: 80 }).toFile(file.path + '.opt.jpg');",
"}",
"vscode.window.showInformationMessage(`Optimised ${ this.files.length } image(s)`);"
]
},
{
"name": "Count words in text file",
"icon": "list-ordered",
"accept": ".txt,.md",
"effect": [
"const text = await this.files[ 0 ].text();",
"const wordCount = text.match(/\\b\\w+\\b/g)?.length ?? 0;",
"vscode.window.showInformationMessage(`${ this.files[ 0 ].name }: ${ wordCount } words`);"
]
}
]
}
Browser-scoped side effects (scope: "browser") do not support accept; the file picker round-trip only applies to the Node-side effect path.
Drop size limits -- when a dropped file has no on-disk path (typically a drag from a web page), the webview base64-encodes the contents and the extension writes them to a unique sub-directory under os.tmpdir()/quack-drop-*/, cleaned up after the effect completes. Each file is capped at 100 MB and each batch at 250 MB; oversized drops are silently skipped client-side and reported as Dropped files exceeded the size limit server-side. Symlinks are rejected at read time. The this.files[*].path always points at a regular file Quack has been told about.
Analyser scripts
A bill can declare an analyser script that contributes additional rows to the Document section of the sidebar.
The script returns a flat key/value object. Each entry becomes a row alongside the built-in file/size/title/lang/charset rows. Values are rendered with the theme's link colour and tooltipped with the contributing bill's title. The reserved keys title, lang, and charset override the built-in HTML extraction; any other key is shown verbatim (or under its analyserLabels label if provided).
Analysers run in the standard bill sandbox with the usual vscode, require, Model, dialog helpers, and progress callback. They are subject to quack.billTimeout; thrown errors silently collapse to an empty contribution so one broken analyser cannot poison the panel.
{
"id": "word-count",
"title": "Word count",
"scope": "html",
"analyser": [
"const words = this.body.replace(/<[^>]+>/g, ' ').match(/\\b\\w+\\b/g) || [];",
"return { wordCount: words.length, readingTimeMin: Math.ceil(words.length / 200) };"
],
"analyserLabels": {
"wordCount": "Word count",
"readingTimeMin": "Reading time (min)"
}
}
Bill subscribers
Bills can react to VS Code events or fire on a cron schedule by declaring one or more entries in a subscribers array. Each subscriber has its own effect script and runs independently of the bill's check / fix lifecycle.
Each entry takes the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Display label for logging and debugging. | |
event |
string |
Yes | One of: onDidSaveTextDocument, onDidOpenTextDocument, onDidCloseTextDocument, onDidChangeTextDocument, onDidChangeActiveTextEditor, onDidChangeTextEditorSelection, onDidChangeConfiguration, onDidChangeWindowState, onDidCreateFiles, onDidDeleteFiles, onDidRenameFiles, or cron. |
schedule |
string |
(when event is cron) |
5-field cron expression: minute / hour / day-of-month / month / day-of-week. |
scope |
string \| string[] |
Optional scope override for this subscriber (defaults to the bill's scope). |
|
conditions |
string \| string[] |
Optional gate script returning a boolean. The effect runs only when truthy. | |
effect |
string \| string[] |
Yes | The script that runs when the event fires. |
icon, category |
string |
Cosmetic. |
The effect (and conditions) script runs in the standard bill sandbox with the usual vscode, require, Model, dialog helpers, and progress callback. In addition:
eventis the VS Code event payload (e.g. theTextDocumentforonDidSaveTextDocument, theFileCreateEventforonDidCreateFiles). Forcron,eventisnull.billis a mutable proxy on the bill manifest. Property assignments are captured byBillOverrideStoreand overlaid on the on-disk bill for the rest of the window session; they are cleared on the next extension reload (no on-disk writes).
{
"id": "auto-save-warn",
"title": "Warn on >100KB save",
"subscribers": [
{
"event": "onDidSaveTextDocument",
"scope": "html,htm",
"effect": [
"const size = Buffer.byteLength(event.getText(), 'utf8');",
"if (size > 100_000) vscode.window.showWarningMessage(`Saved ${ (size/1024).toFixed(1) }KB`);"
]
},
{
"event": "cron",
"schedule": "0 9 * * 1-5",
"effect": "vscode.window.showInformationMessage('Good morning — daily lint reminder')"
}
]
}
Browser Preview
Click the Preview button in the Quack sidebar header (or run Quack: Open Browser Panel) to open a live preview of the active document in a VS Code panel. The document is served from a local HTTP server. The panel updates as you type (debounced 300 ms), serving the unsaved content directly from memory. When the file is saved, the memory buffer is cleared and the panel reloads from disk.
Set quack.useIntegratedBrowser to true to open the preview in VS Code's built-in Integrated Browser tab instead of Quack's bundled webview. Reloads, document-save refreshes, and sidebar browser-state updates still target the Integrated Browser tab; the setting silently falls back to the bundled panel on VS Code builds older than 1.109.
When the Browser Preview panel is focused, the sidebar will clear its bill list if the current file has no browser bills defined.
Browser bills
Add a browser field to any bill to run JavaScript or TypeScript inside the preview page on each load. Browser bill code runs in the live browser context and has access to window, document, and all other browser APIs.
Browser bills return the same format as check:
{
"id": "layout-check",
"title": "Layout check",
"browser": [
"const main = document.querySelector('main');",
"return { result: main ? 'pass' : 'fail', message: 'Page must contain a <main> element.' };"
]
}
Results are not displayed in the sidebar -- browser bills run silently in the background on every page load.
If the bill defines a prepare field, the prepare phase runs automatically before the browser script executes. Its return value is available as context inside the browser bill, matching the variable name used in check.
Browser bills have access to the following top-level variables, mirroring the check/fix execution context as closely as possible:
| Variable | Value |
|---|---|
body |
The full document source as a raw string |
bill |
The bill object (id, title, description, etc.) |
config |
Resolved configuration for this bill |
filename |
Absolute path to the active document |
context |
The merged object from all bills' prepare results |
vscode.window.showInformationMessage(msg, ...items) |
Shows a VS Code info notification; returns a Promise resolving to the clicked item label, or undefined if dismissed |
vscode.window.showWarningMessage(msg, ...items) |
Same, as a warning notification |
vscode.window.showErrorMessage(msg, ...items) |
Same, as an error notification |
vscode.revealInEditor(filePath, searchText) |
Opens a file in the editor and scrolls to the first occurrence of searchText |
Browser-context side effects
Side effects with scope: "browser" target the browser context instead of the editor. They use a browser property (not effect) for their code, which runs inside the preview window. Browser side effects appear in the sidebar's Side Effects section only while the Browser Preview panel is open.
{
"id": "browser-tools",
"title": "Browser Tools",
"sideEffects": [
{
"name": "Log performance",
"icon": "pulse",
"scope": "browser",
"browser": [
"const entries = window.performance.getEntriesByType('resource');",
"console.table(entries.map(e => ({ name: e.name, duration: e.duration })));"
]
}
]
}
Context menu
Right-clicking any element in the Browser Preview panel opens a context menu. The available items depend on the element type:
| Item | Available on | Action |
|---|---|---|
| Reload | All elements | Reloads the preview page |
| Copy Text | All elements | Copies the element's innerText to the clipboard |
| Copy HTML | All elements | Copies the element's innerHTML to the clipboard |
| Copy Image URL | Images (or children of images) | Copies the image src to the clipboard |
| Copy URL | Links (or children of links) | Copies the link href to the clipboard |
| Copy Source | Video/audio elements | Copies the media src to the clipboard |
| Find in Code | Elements with source data | Selects the element's source range in the active editor |
| Inspect | All elements | Highlights the element with a 3-second blue outline, logs it to the browser console, and opens VS Code developer tools for the webview |
Find in Code is only available when Quack can determine the element's position in the source file. It is supported for static HTML files served by Quack's built-in server. PHP and other dynamic modes do not currently support it.
CMD/CTRL+R
Press Cmd+R (Mac) or Ctrl+R (Windows/Linux) while the preview is focused to reload the page. This is equivalent to the Reload context menu item.
CMD/CTRL+click
Hold Cmd (Mac) or Ctrl (Windows/Linux) and click any link in the preview to open it in the system's default browser instead of navigating the preview.
Browser dialog APIs
Browser bill code runs after the page has fully loaded, so window.load listeners will not fire. Use top-level code instead.
As alert, prompt, and confirm are not available in the VS Code webview sandbox, Quack replaces them with VS Code equivalents:
| Call | VS Code equivalent |
|---|---|
alert(message) |
Info notification (fire-and-forget) |
await prompt(message, default) |
Input box -- returns the entered string, or null if cancelled |
await confirm(message) |
Modal message -- returns true (OK) or false (Cancel) |
Because prompt and confirm are asynchronous, bill code that uses them must await their return values:
{
"id": "confirm-example",
"title": "Confirm example",
"sideEffects": [
{
"name": "Rename heading",
"scope": "browser",
"browser": [
"const h1 = document.querySelector('h1');",
"const text = await prompt('New heading text', h1?.textContent ?? '');",
"if (text && h1) h1.textContent = text;"
]
}
]
}
Snippets
The Snippets panel aggregates every snippet available to the active workspace and lets you drag any of them straight into the editor. Sources discovered automatically:
- User -- the same snippet files VSCode reads (
<user-snippets>/*.jsonand<user-snippets>/*.code-snippets). - Workspace --
.code-snippetsfiles in the active workspace's.vscode/directory. - Extensions -- snippets contributed by every installed extension via its
contributes.snippetsentry. - Bills -- snippets declared on Quack bills via the new
snippetsfield (see below).
Views
Toggle between two layouts with the view-mode action in the panel title bar:
- Tree view -- the snippet name sits next to the source icon with a smaller description line below it. Rows are grouped by source then by language/extension/bill.
- Thumbnail grid -- a wider grid of tiles. A tile shows the snippet's icon (or its thumbnail image when set) plus the name as a two-line caption underneath. Thumbnails share a fixed 16:9 aspect ratio so the grid stays uniform.
Organising snippets
- Custom folders -- click the New folder icon on a source row to create a folder inside it. The folder name is collected through a VSCode input box. Drag any snippet onto a folder header to move it; drag onto the source row, or onto the blank space below the tree, to put it back at the source root. The layout is stored globally in the editor's state, so it follows you across workspaces and machines that share the same VSCode profile.
- Hide / Show hidden -- right-click a snippet for Hide or a folder for Hide Folder. Hidden ids are stored globally (across windows and workspaces). When anything is hidden a Show hidden entry appears in the panel's filter dropdown that brings every hidden item back into view.
- Collapsed state -- expanded/collapsed state for every source root and folder is stored globally too, so the tree opens the same way across sessions.
Right-click menus
| Target | Items |
|---|---|
| Snippet card | Copy Snippet Code, Reveal in Finder/Explorer, Hide/Show, Edit, Delete |
| Custom folder | Hide Folder/Show Folder, Rename Folder, Delete Folder |
| Auto-group folder (per-extension / per-bill) | Hide Folder/Show Folder |
Edit opens the underlying snippet file (the bill manifest for bill snippets) and jumps the cursor to the line where the snippet is defined. Delete asks for confirmation and rewrites the source JSON in place; it is disabled for read-only sources such as installed extensions. Rename Folder and Delete Folder only appear on custom folders -- the per-extension and per-bill auto-groups are derived from their source labels and can't be renamed or removed. Deleting a folder moves its snippets back to the source root.
Adding a new snippet
User and Workspace source rows have a + action button. Clicking it:
- Opens (or creates) a language-scoped snippets file in the active editor:
<languageId>.jsonfor User snippets and<languageId>.code-snippetsfor Workspace snippets. With no active editor or aplaintextdocument, it falls back tosnippets.json/snippets.code-snippets. - Inserts a stub entry at the end of the root object using VSCode's snippet tab stops, so you can immediately tab through name -> prefix -> body -> description.
- Leaves the file dirty so nothing is written to disk until you save.
Drag-to-editor
Dragging a snippet from the panel into an open editor inserts its body at the drop point. The drag uses the application/vnd.quack.snippet+json MIME and the drop edit is tagged with the quack.snippet kind, so you can pin Quack as your preferred snippet drop handler:
"editor.dropIntoEditor.preferences": [
"quack.snippet"
]
See VSCode's drop into editor documentation for the full grammar; entries can be scoped per language or per workspace, or used to push Quack down the priority list.
Augmenting snippets
Beyond the standard VSCode fields (prefix, body, description, scope), Quack supports four additional fields on any snippet entry. They all live in the same JSON file the snippet itself does. Use them in user, workspace, extension, or bill snippets -- the loader treats every source the same way.
| Field | Type | Purpose |
|---|---|---|
icon |
string |
A codicon name. Replaces the default symbol-snippet icon in the tree row and (when no thumbnail is set) the thumbnail tile. |
thumbnail |
string |
An image URL (https://...), data URI (data:image/...;base64,...), or local path. Relative paths resolve against the snippet file's directory (or the bill's base path for bill snippets). When set, the image replaces the icon inside the tile and the name renders as a two-line caption below. |
prepare |
string | string[] |
JavaScript or TypeScript that runs before the snippet body is inserted. Can rewrite or replace the body. |
transform |
string | string[] |
JavaScript or TypeScript that runs after the body has been "virtually" inserted into the document. Returns a replacement for the document, or a structured object describing a snippet insert plus an optional head insertion. |
Example -- icon and thumbnail
// .vscode/components.code-snippets
{
"Action card": {
"scope": "html",
"prefix": "card",
"body": [
"<article class=\"action-card\">",
" <h2>${1:Heading}</h2>",
" <p>${2:Body}</p>",
"</article>"
],
"icon": "preview",
"thumbnail": "./previews/action-card.png"
}
}
The image is loaded inside the webview via a vscode-resource:// URI, so any path under your workspace, the user snippets directory, the contributing extension, or the bill's directory is accessible without further configuration. Remote http(s):// URLs and inline data: URIs are also supported.
prepare
prepare is invoked with a context object describing the drop. Return a string to replace the body that will be inserted, or return nothing to leave the body unchanged.
Execution context (this):
| Property | Description |
|---|---|
this.body |
The full text of the target document at drop time. |
this.snippet |
The snippet content as a single string (joined with \n when authored as an array). |
this.position |
Zero-based character offset of the drop position. |
this.language |
The target document's languageId. |
this.toString() |
Returns this.snippet, so a prepare that returns ${this} still resolves to the snippet content. |
{
"Timestamped log": {
"prefix": "log",
"body": [ "console.log('[${1:tag}]', ${2:value});" ],
"prepare": [
"const tag = new Date().toISOString();",
"return this.snippet.replace('${1:tag}', tag);"
]
}
}
A prepare-only snippet is inserted as a regular SnippetString, so VSCode's tab stops still work after the body is rewritten.
transform
transform runs after prepare (if any) and operates on the entire document with the snippet body already merged at the drop position. It can return a string, a structured object, or nothing (to fall back to a plain snippet insert).
Use transform when an insert needs to touch the file outside the snippet itself -- adding an import, registering the snippet in a manifest, sorting a list it landed in, etc.
Execution context (this):
| Property | Description |
|---|---|
this.body |
The full document text with the snippet already inserted at this.position. |
this.snippet |
The (possibly prepare-rewritten) snippet content. |
this.position |
The drop offset, in the merged document. |
this.language |
The target document's languageId. |
this.toString() |
Returns this.snippet. |
Return shapes:
| Return | Behaviour |
|---|---|
string |
Replaces the entire document. Tab stops are not honoured because the document is a fully resolved string. |
{ bodySnippet: string } |
Replaces the entire document with bodySnippet parsed as a SnippetString. Tab stops anywhere in bodySnippet are active and linked across the whole document -- use this when you need to insert into the snippet body and somewhere else (e.g. a matching import) while keeping a single linked tab-stop session. |
{ snippet: string, headInsertion?: { position: number, text: string } } |
Inserts snippet as a SnippetString at the drop point (tab stops work), and optionally inserts headInsertion.text (with a trailing newline) at headInsertion.position -- handy for dropping an import at the top of the file while the cursor lands inside the snippet. |
undefined |
Falls back to the default plain snippet insert. |
{
"Named export": {
"prefix": "exp",
"body": [ "export const ${1:name} = ${2:value};" ],
"transform": [
"const updated = this.body;",
"if (updated.includes(\"export *\")) return undefined;",
"return updated.replace(/(\\nexport \\{)/, '\\nexport { ${1:name},' );"
]
}
}
{
"Use hook": {
"prefix": "uh",
"body": [ "const ${1:value} = use${2:Hook}();" ],
"transform": [
"if (this.body.includes(\"import { use\")) return undefined;",
"return {",
" snippet: this.snippet,",
" headInsertion: { position: 0, text: 'import { use' + '${2:Hook}' + ' } from \\'./hooks\\';' }",
"};"
]
}
}
Loading from external files
Both prepare and transform accept either inline source (string or string[]) or a relative path to a .js / .ts file:
{
"Module skeleton": {
"prefix": "mod",
"body": [ "export const ${1:name} = {};" ],
"prepare": "./snippets/prepare-module.ts",
"transform": "./snippets/transform-module.js"
}
}
Paths resolve relative to the snippet file (or the bill directory for bill snippets). The scripts run with the same sandboxed require Quack bills use, so they can import other workspace modules.
Bill-contributed snippets
A QABill can declare its own snippet entries via a snippets field. The shape is identical to a VSCode snippet object, with the same Quack additions:
// .quack/snippet-bill.json
{
"id": "ui.cards",
"title": "UI: Cards",
"snippets": {
"Action card": {
"scope": "html",
"prefix": "card",
"body": [
"<article class=\"action-card\">",
" <h2>${1:Heading}</h2>",
" <p>${2:Body}</p>",
"</article>"
],
"icon": "preview",
"thumbnail": "./previews/action-card.png"
}
}
}
Bill snippets appear under the Bills source root in the panel, grouped by bill title. Relative thumbnail, prepare, and transform paths resolve against the bill's directory.
Settings reference
| Setting | Type | Default | Description |
|---|---|---|---|
quack.bills |
array |
[] |
Bills defined inline in settings.json. |
quack.billImports |
array |
[] |
Glob patterns, file paths, or directory paths pointing to .json bill files. Bill code can reference .js or .ts files. |
quack.workspaceBills |
boolean |
false |
When enabled, automatically loads .json bill files from the .quack directory at the workspace root (top-level only). Script paths within each bill resolve relative to the .quack directory. |
quack.billsDisabled |
array |
[] |
IDs of bills that are currently disabled. Written to .vscode/settings.json (workspace scope). Managed by the sidebar toggles -- not shown in the Settings UI. |
quack.autoCheck |
boolean |
false |
Re-run all enabled bills automatically after each edit (debounced 500 ms). |
quack.checkAfterFix |
boolean |
true |
Re-check bills automatically after applying a fix. Fix on a single bill re-checks all previously checked bills; Fix All re-checks every enabled bill. |
quack.showSideEffects |
boolean |
true |
Show the Side Effects section in the sidebar. |
quack.compactMode |
boolean |
false |
Condense bill cards: hides descriptions and reduces padding. Bill descriptions are still visible as tooltips. |
quack.fileExtensions |
array |
[] |
File extensions Quack activates on (e.g. "html", ".js"). Leave empty to activate on all file types. |
quack.billTimeout |
number |
60 |
Maximum number of seconds a bill's prepare, check, fix, or side effect is allowed to run before it is cancelled and reported as an error. Set to 0 to disable the timeout. |
quack.useIntegratedBrowser |
boolean |
false |
Route the browser preview through VS Code's built-in Integrated Browser (workbench.action.browser.open) instead of Quack's bundled webview panel. Requires VS Code 1.109 or newer; silently falls back to the bundled panel on older builds. |
Support
If this is useful to you and you'd like to support its development, you can buy me a coffee on Ko-fi - always optional, always appreciated.
License
MIT - see LICENSE.
