Azure DevOps Board
Your assigned Azure DevOps work items in VS Code, grouped by state. Two views
over the same data:
- Sidebar tree — quick access, native keyboard navigation and context menus.
- Board panel —
Azure Board: Open Board opens a React webview in an editor
tab: category tabs, a ⌘K palette, and a detail sheet. Works from 320px up.
Click any item to read its description and acceptance criteria as rendered
markdown.
Install
Search Azure DevOps Board in the Extensions view, or:
ext install pratyakash.azure-devops-board
Why it needs no credentials
Every call shells out to the Azure CLI (az boards), so authentication, token
refresh, and org defaults are the CLI's problem. The extension stores no secrets
and has zero runtime dependencies.
Requirements
The Azure CLI with its
Azure DevOps extension:
az extension add --name azure-devops
Nothing else. If az boards works in your terminal, this works.
Setup
Install the extension, open the Azure Boards view in the activity bar, and
click Sign in to Azure DevOps. You are asked for:
- your organization URL, e.g.
https://dev.azure.com/my-org
- your project name
Both are saved to settings, then az login runs in a terminal. The board loads
by itself once the sign-in completes — no manual refresh.
Already configured az devops configure --defaults? Those are used when the
settings are left empty.
Accounts
The board shows work items assigned to whoever az is signed in as — that is
what @Me resolves to in the query. Run Azure Board: Switch Account (or click
the account row in the sidebar) to sign in as someone else, or sign out.
| Choice |
Command it runs |
| Sign in |
az login --allow-no-subscriptions |
| Sign out |
az logout |
--allow-no-subscriptions is not incidental: an account with Azure DevOps access
but no Azure subscription — the normal case for a developer — fails plain
az login with "No subscriptions found" and ends up signed out.
The command runs in a terminal, and the extension never sees the credential.
Signing in is a browser flow, so it is handed to az, which already manages the
token. There is deliberately no token box here: that would mean this extension
storing a secret the Azure CLI is responsible for.
Settings
| Setting |
Default |
Meaning |
adoboard.organization |
"" |
Organization URL. Empty falls back to the az devops configure default. |
adoboard.project |
"" |
Project name. Same fallback. |
adoboard.promotedStates |
["Ready for QA"] |
States given a tab of their own instead of being folded into their Azure DevOps category. A state your process doesn't use simply never matches. |
What it does
- Board tree — one WIQL query for every open item assigned to you, bucketed
by state (largest bucket first). States
Done, Completed, Closed,
Removed, and Resolved are excluded.
- Sprint filter — the
Sprint: … row at the top of the tree (or the funnel
icon in the view title) opens a searchable picker: All sprints, a
Current sprint shortcut (whichever iteration contains today), then every
project iteration newest-first. The active choice is marked, shown in the
tree row, and persists per workspace. Picking a grouping node such as
MyProject\Previous Sprints includes its children.
- Current-branch item — pinned at the top when the branch name carries a
ticket id (
fix/next_71253 → 71253).
- Detail view — description + acceptance criteria converted from ADO's HTML
to markdown and opened in a preview. A virtual document, not a webview.
- Category tabs, not columns. Azure DevOps gives
User Story 27 states, so
a column-per-state board rendered 13 columns and needed ~3,500px. Each state
carries a category, and those collapse the same board to 3 tabs — To do
(71), In progress (40), Resolved (9). The real state is kept as a heading
inside each tab, so nothing is lost. One column below 640px, a grid above.
- ⌘K palette to jump to any work item by id, title, type, or state.
- Detail sheet — clicking a card slides in its description and acceptance
criteria, rather than taking over an editor tab.
- Stat header — open, in progress, blocked, and the current branch's item,
over a bar showing the whole backlog's distribution.
- Sprint dropdown beside the text filter; neither round-trips to
az to
filter. Iterations are labelled by path, not bare name — a project commonly
has both MyProject\22 and MyProject\Previous Sprints\22. The live sprint is hoisted to
the top and marked — current, exactly once, so no two options share a value.
- Drag a card onto a state section to change its state. Sections the card
cannot legally enter dim during the drag. To cross categories, hover the
target tab for a moment mid-drag and it springs open, then drop.
- Change state from the card via its
Move to… select, which lists only
states valid for that work item's type — on a real Agile process 53% of
state/type combinations are invalid (a Task has 7 states and no
Ready for Dev, a User Story has 27), so an unfiltered list would mostly
offer moves the server rejects. Every move raises a toast with an Undo.
- Commands — Open Board, Refresh, Switch Account, Select Sprint, Open in
Browser, Copy Work Item Id, Open Work Item for Current Branch.
- Signed out, it says so and nothing else. No empty board, no sprint filter,
no board button — just one sign-in action, in the sidebar and the panel alike.
Architecture
src/az.ts `az` CLI calls, WIQL building pure Node, tested
src/format.ts HTML->markdown, iterations, URLs pure, tested
src/board.ts view model + message protocol pure, tested, shared
src/sprint.ts sprint selection state shared by both views
src/tree.ts sidebar TreeDataProvider VS Code glue
src/panel.ts webview host, CSP, messaging VS Code glue
src/webview/App.tsx React kanban app esbuild
src/webview/globals.css Tailwind v4 theme @tailwindcss/cli
src/webview/components/ui/ shadcn/ui components vendored by the CLI
src/webview/lib/utils.ts shadcn `cn` helper
src/markdown.ts tiny markdown parser pure, tested
Work item bodies are HTML authored by anyone with board access. format.ts
converts them to markdown, markdown.ts parses that, and Markdown.tsx renders
React elements — nothing reaches the DOM as markup, and only http(s) links
become clickable (isHttpUrl, enforced again before openExternal).
Styling
The panel uses shadcn/ui on Tailwind v4 and Radix
primitives. shadcn is a generator, not a dependency — npx shadcn@latest add
copies component source into src/webview/components/ui/, and that code is
owned and editable here.
Two things the setup needs that a Next/Vite project gets for free:
- The
@/* alias lives in the root tsconfig.json. The shadcn CLI resolves
its aliases against that file specifically; without it the CLI writes to a
literal directory named @. esbuild mirrors the alias in esbuild.mjs.
- CSS is a separate build step. esbuild does not run Tailwind, so
npm run compile invokes @tailwindcss/cli over globals.css and esbuild
only bundles the TSX. globals.css declares @source explicitly because the
input file is not at the project root.
Dark mode keys off VS Code's own vscode-dark / vscode-high-contrast body
class via a Tailwind @custom-variant, not prefers-color-scheme — only the
body class follows the user's editor theme rather than their OS setting.
board.ts deliberately imports nothing — the webview bundle pulls from it, so a
vscode or node: import would either break esbuild or drag the extension host
into the browser bundle.
Develop
npm install
npm test # Node's built-in runner, native TS — no build step
npm run type:check # both tsconfigs: extension (Node/CJS) and webview (DOM/JSX)
npm run compile # tsc for the extension, esbuild for the webview
npm run watch # esbuild watch for the React app
Press F5 to launch an Extension Development Host.
There are two tsconfigs because the two halves target different runtimes: the
extension is CommonJS on Node with no DOM, the webview is ESM in a browser with
JSX. tsconfig.webview.json only type-checks; esbuild does the bundling.
Tests cover the pure logic — HTML conversion, branch parsing, WIQL escaping, URL
building, board grouping, and message validation. tree.ts, panel.ts,
doc.ts, and extension.ts are VS Code API glue, exercised by running it.
Known limits
- State is the only writable field. No comments, assignment, or time logging.
- A move patches one card, it does not refetch. The card is moved locally on
drop, the write runs, and only that card is confirmed or sent back to the state
it came from. Nothing else re-renders and the scroll position is kept. The
sidebar tree is patched in step, also without a second query. The trade-off:
if a workflow rule silently rewrites the state to something other than what
was asked for, the board won't notice until the next Refresh.
- HTML conversion is regex-based — it handles what ADO's editor emits
(
div/p/br/li/b/i/a/img). Tables and deeply nested lists will
flatten. Swap in turndown if that starts mattering.
- Sprints come from the project iteration list, not a team backlog. The
@CurrentIteration WIQL macro and az boards iteration team list both need a
team context that az boards query can't supply, so "current sprint" is
computed by comparing today against each iteration's start/finish dates. A gap
between sprints means no current sprint, and the shortcut is hidden.
- Tree grouping is by state, not by sprint — the sprint is a filter.
- With a sprint filter active, the branch-pinned item only appears if that work
item is in the selected sprint.
- Items are fetched once per refresh; there's no background polling.
- The panel no longer matches your editor's exact colors. It follows the
light/dark mode but uses its own violet palette, which is the point of the
theme and the cost of it. The sidebar tree is still fully native.
- The webview bundles React, Radix, and Tailwind output — ~336 KB of JS and
~50 KB of CSS in a 143 KB package. The tree view costs nothing by comparison.
- Microsoft's
@vscode/webview-ui-toolkit is deprecated, which is why the panel
uses shadcn/Radix rather than an official VS Code component library.