Repository navigation
v0.39.0
What's changed
Prepare the repo for cloud sessions
- CLAUDE.md now carries the project knowledge that lived only in local
notes: layout, the azure-devops-ui rule, build and test commands, the mock
harness, code rules that have bitten us, the release steps and how live
testing works. - .claude/settings.json: a SessionStart hook that installs the extension's
and the auth function's dependencies in cloud sessions only. - npm run shots: starts the mock harness and saves headless Chrome
screenshots of the main pages in light and dark (no new dependency), so
UI can be checked without a browser session. The mock takes ?theme=. - publish.yml: a push to main whose version has no tag yet is a release; the
workflow publishes, then creates the tag and GitHub release. Cloud
sessions can't push tags. Pushing a tag yourself still works.
Keep the screenshot Chrome profile out of the repo
Chrome installed a managed extension into the profile under dist-dev/, and
vitest collected its *.test.js files (71 failing files locally; CI was
unaffected because dist-dev is ignored). The profile now lives in the
system temp directory.
Point CLAUDE.md at the gh-repos-sandbox test repo
Describes what the sandbox holds and that anything in it is safe to change,
so local and cloud sessions test there instead of in real repos.
Add a device-flow script for the dev GitHub App's user token
Gets a ghu_ user token for the sandbox-only dev app without a client secret
or callback, the same kind of token the extension uses in ADO. It lists the
repos the token can reach (warning if the app is installed on all of them)
before printing the token, and copies it to the clipboard where it can.
Drop a meaningless expiry line from dev-token.mjs
Add a live data mode to the mock harness
LIVE=1 GH_SANDBOX_TOKEN=... npm run dev (and npm run shots -- --live) sends
api.github.com to the real network with the injected token while ADO stays
mocked, so cloud sessions can test the UI against TrueNorthIT/gh-repos-sandbox.
The token is defined into the dev bundle only; build.mjs refuses LIVE in a
production build and publish.yml fails if dist holds a token or live marker.
A pure repo guard (src/dev/liveGuard.ts, table-tested) refuses any request that
addresses another repository before it leaves the page.
Add a PreToolUse hook as a second sandbox-token guard
Blocks Bash commands that use GH_SANDBOX_TOKEN or a ghu_ token while naming a
GitHub repository other than TrueNorthIT/gh-repos-sandbox. Sample inputs in
.claude/hooks/sandbox-only.test.sh.
Catch gh api repos/… without a leading slash in the sandbox-token hook
The PreToolUse hook only recognised API paths written as "/repos/owner/name",
but gh api takes them without the leading slash too. So a command that used
the sandbox token against another repository in that form
(gh api repos//) passed the hook, and only the cloud
session's own egress proxy stopped it. Found on the first cloud-session check.
The pattern now accepts "repos/" at a word start (after whitespace, a quote,
"=" or "/") as well as after a slash, which still ignores names like
gh-repos-sandbox. Three new samples in sandbox-only.test.sh cover the bare
form, a quoted path with a query, and the sandbox itself still passing.
Route live mode through a same-origin proxy in the dev server
Live mode used to define the sandbox token into the dev bundle and send it
from the page straight to api.github.com. In a cloud session that cannot
work: the egress proxy removes Authorization from cross-site browser
requests (Sec-Fetch-Site: cross-site), so GitHub saw every call as
anonymous. It also meant the token sat in a JavaScript bundle.
Now the token never reaches the page:
- build.mjs --dev runs a small Node http server in front of esbuild's
serve. In live mode it mounts src/dev/ghProxy.ts at /__gh/…, and
src/dev/live.ts rewrites api.github.com requests to that path. The bundle
gets only a LIVE flag; there is no token define any more. - ghProxy reads GH_SANDBOX_TOKEN in Node and adds it to each request. It
passes on only a short allow list of the page's headers. - It proxies api.github.com only: a full or protocol-relative URL, a
backslash or a control character in the path gets a 400. - The same liveGuard decision runs again on every proxied request,
including each api.github.com redirect hop, and refuses with a 403 and a
"[live guard] refused (dev server)" line on stderr. The in-page guard
stays. - Log and artifact redirects are followed by hand (redirect: "manual").
The token and the page's headers go to api.github.com only, never to the
blob host. Node's fetch also drops Authorization on a cross-origin
redirect (checked on Node 22.23), but we don't rely on it. - When HTTPS_PROXY is set, build.mjs re-runs itself with
NODE_USE_ENV_PROXY=1, so the dev server's fetch goes through the
session's egress proxy rather than around it. - shots.mjs looks up its live run and job through /__gh too, and keeps
the dev server's stderr so guard refusals show.
tests/ghProxy.test.ts covers the host and path checks, server-side guard
refusals (nothing sent, logged), header forwarding, sandbox-id resolution
and the redirect rules.
In cloud sessions GitHub GraphQL is still refused by the egress proxy, so
GraphQL-backed pages (PR overview, releases) can't be checked live there.
CLAUDE.md says so, and also notes that the PreToolUse hook isn't active
once a second repo is attached to the session. No UI changed; nothing was
hand-rolled.
Make a fresh cloud session work without manual steps
The first cloud session hit four problems, all fixed here:
- Hooks didn't load once the sandbox was attached. Live mode needs
TrueNorthIT/gh-repos-sandbox attached (the egress proxy refuses API calls
for any other repo), and a session with two repos is rooted at /home/user,
so .claude/settings.json never loads: no dependency install, no
sandbox-token guard. .claude/cloud-setup.sh is meant to be pasted as the
environment's setup script. It merges the same two hooks into the
user-level ~/.claude/settings.json, pointing into the repo (tested
against a throwaway HOME: keeps existing settings, adds nothing on a
second run). - The install hook was inline and ran twice in some sessions. It moves to
.claude/hooks/session-start.sh, used by both settings files. It takes a
lock and records each package-lock.json hash, so a second or concurrent
run is a no-op (checked: two at once, then 0.02s for a rerun). - Screenshots hung. Full Chrome's --screenshot hangs in cloud containers,
even on a blank page, and Chrome won't run as root with its sandbox.
shots.mjs now prefers Playwright's chrome-headless-shell under
/opt/pw-browsers (SHOTS_CHROME overrides), adds --no-sandbox as root,
exits non-zero when a shot fails (it exited 0 when all six failed),
deletes each PNG first so a stale one can't pass, and includes GitHub's
message when its live run lookup fails. - The egress proxy's refusals were opaque. The dev proxy now explains
them once on stderr as "[gh proxy] …": attach the sandbox, or GraphQL
is refused in cloud sessions. GitHub's own 403s are left alone.
Also fixed in sandbox-only.mjs: a shell operator right after a repo name
(…/gh-repos-sandbox; echo) was read as part of the name, which blocked a
sandbox command. New samples cover it.
CLAUDE.md gets a "Cloud sessions" section listing the environment
settings: both repos, the setup script, GH_SANDBOX_TOKEN.
Ignore agent worktrees under .claude/worktrees
Parallel subagents work in git worktrees that Claude Code creates under
.claude/worktrees/. They are scratch checkouts, merged back by branch, and
must never be committed.
Add a date filter to Pushes and mark force pushes more clearly
Native ADO's Pushes page filters by date as well as by who pushed. GitHub's
activity API has time_period (day, week, month, quarter, year), so the
filter bar gains a single-select Date dropdown (DropdownFilterBarItem) that
GitHub answers server-side, beside the existing "Pushed by" box.
Both filters now live in the hash (pushedBy, pushPeriod in lib/pushesHash.ts,
beside browseState.ts), so a reload or a shared link keeps them, and they
survive a branch switch the way the PR list filters do. The empty state says
what was filtered and offers "Clear filters".
The force push pill was a plain grey pill identical to "Branch created"; it is
now outlined with a Warning icon and an aria label, so it stands out in the row.
The mock activity route honours time_period, and the shots script has a
pushes-filtered page. No hand-rolled markup.
Filter Commits by author and date on GitHub, over REST
Native ADO's Commits filter bar has author, from date and to date. Ours had
only a keyword that searched the rows already loaded, and the list came from
GraphQL, which cloud sessions cannot reach at all.
The list now comes from REST GET /repos/{o}/{r}/commits (lib/commitsRest.ts),
which takes sha, path, author, since and until, so the author, date and path
filters are answered by GitHub and "Load more" keeps paging the filtered
history (Link rel="next" page numbers). REST has no PR, check status or
signature, so those are filled in by one best-effort GraphQL lookup of the
page's oids; if it fails the rows still show without them. An empty repo
(409) or unknown ref (422) is an empty list rather than an error.
The filter bar gains TextFilterBarItems for Author (login or email), From date
and To date. azure-devops-ui 2.x has no date picker and TextField cannot be
type=date, so the dates are typed as dd/mm/yyyy (yyyy-mm-dd also accepted),
read as local days, and a warning MessageCard explains a date that does not
parse. The keyword stays a loaded-rows filter, now over the whole message.
The graph still hides while any filter is on.
All filters are kept in the hash (cAuthor, cFrom, cTo, cPath, cText) and carried
across a branch switch and into a commit and back. The empty state names the
filters and offers "Clear filters". FileTabs' history still uses the GraphQL
listCommits, unchanged. The mock answers the REST list with every filter, and
the shots script has a commits-filtered page. No hand-rolled markup.
Add branch policies, rename, set as default and tag/commit compare to Branches
Native ADO's branch row menu has "Branch policies", and its compare picker takes a
branch or a tag. GitHub can do all of this over REST, so the Branches hub now
matches native more closely.
- Branch policies: a read-only page (view=policies&polBranch=…, lib/policyHash.ts)
from the row menu, or from the shield on a protected branch's row. It reads
GET /rules/branches/{branch} (read access is enough), the classic
GET /branches/{branch}/protection (admin only) and GET /branches/{branch}.
lib/branchPolicies.ts maps the rules onto native's sections: "Require a
minimum number of reviewers", "Check for linked work items" (not available on
GitHub), "Check for comment resolution", "Limit merge types", "Build
Validation" (required status checks, strict = expires when the branch is
updated), "Automatically included reviewers" (code owners) and the
GitHub-only rules under "Branch protection". When the classic rule can't be
read, a message says so and the checks GitHub shows readers are listed.
"Edit on GitHub" opens the repository's rulesets settings; each rule links to
its ruleset or the branch protection settings. Built from Header, Card, a
disabled Toggle per policy (as native shows them without "Edit policies"),
Table, MessageCard, ZeroData and Link. - Rename branch: row menu item and a Dialog with the New branch name checks,
POST /branches/{branch}/rename. A 403 says the default or a protected branch
needs admin; favourites and the remembered compare branch follow the rename. - Set as default branch: shown to repository admins only (permissions.admin on
GET /repos/{repo}), with a confirmation Dialog, PATCH /repos/{repo}. - Compare: both pickers list branches and tags under headings (Dropdown with
ListBox header items) and offer a commit SHA typed into the search box. Tags
come from REST /tags. "New pull request" is off unless both sides are branches.
Mocks: main has a ruleset with several rules plus a classic rule, release/1.0
only a classic rule; ?scenario=notAdmin (and readOnly) shows the reader's view
and hides "Set as default branch"; refsFail makes rename and set-default 403.
Shots pages: branch-policies, branch-policies-reader, compare-tag.
Hand-rolled: the policy row layout (a flex row of Toggle and text) and the
"From Ruleset N" source line, as azure-devops-ui has no settings-row component.
Add the branch policies and tag compare pages to the live shots
So the REST reads behind them can be checked against the real sandbox: both render there (main has no rules, so the policies page shows its empty state; v0.1.0...main resolves the tag through REST /tags).
Link AB# work item mentions in commit messages
PR pages already turn AB#123 into Azure Boards work items; commit messages
did not, though that is where most teams write them.
The Commits list now shows a compact outlined Pill per mentioned work item on
each row's second line, wrapped in a Link that opens the item in a new tab, with
a Tooltip giving type, title and state once ADO answers. All ids on screen are
looked up in one call through PrPage's useWorkItems (fetchWorkItems), and the
links work before or without that lookup, from the project context alone.
Commit detail shows the same pills in the header line, links each mention in
the Details tab's message, and adds a "Work items" row listing them with the PR
page's WorkItemRow (type, id, title, state; "not found or no access" for ids ADO
does not return). Mentions in code spans and fences are left alone, as on PRs
(lib/commitWorkItems.ts, on top of lib/workItems.ts).
Mock history commits now carry AB# references in headlines and bodies,
including one id the work item mock does not know, and the shots script has a
commit-detail page. No hand-rolled markup.
Files: Clone, copy link to lines, zip download, markdown edit preview, delete folder
Five gaps against native Repos' Files page, all in the Files hub:
- Clone: a header button opening a callout with HTTPS and SSH tabs, the
read-only clone URL with a copy button, and "Clone in VS Code"
(vscode://vscode.git/clone?url=...). The URLs are GitHub's own
clone_url and ssh_url, now read by getRepo. Built from ExpandableButton,
Callout, TabBar, TextField (readOnly) and ClipboardButton. - Copy link to a line or range: selecting lines in the Contents view
(mouse or keyboard) turns the file's "..." item into "Copy link to
lines 5-9"; the link is the Files hub URL with native's line/lineEnd
hash keys, and opening it tints those lines and scrolls to them. The
keys sit beside browseState in lib/lineHash.ts, read through
useHashExtras. A Toast confirms the copy. - Download repository as zip: on the folder "..." menu and the repo menu.
GitHub has no per-folder archive, so it is labelled as the whole
repository at the current ref. The API's zipball redirects to
codeload.github.com, which only allows render.githubusercontent.com
cross-origin (checked against the sandbox), so the extension cannot
fetch it; it opens github.com's archive URL, which the browser
downloads with its own GitHub session. - Markdown preview while editing: the Edit pane of a .md file gets
Edit / Preview tabs (TabBar); Preview renders the unsaved text through
POST /markdown and the same sanitiser and Markdown renderer as the
Preview tab. The editor stays mounted, hidden, so undo survives. - Delete folder: a folder "..." item (not the root) with the same Commit
dialog as file delete, behind the same can-push check. It uses the REST
Git data API: a tree on the head's tree with every file under the
folder set to sha: null, a commit on the loaded head, then the branch
moved without force (a moved branch is refused as stale and offers
Reload) or a new branch created at the commit. A truncated tree is read
from the folder's own recursive tree; if that is truncated too, nothing
is deleted.
Mocks: clone_url/ssh_url on the repo, the Git data writes (tree, commit,
ref update with the staleHead scenario, new ref), a folder's recursive
tree in the truncated monorepo. shots.mjs gains a file-lines page.
Nothing hand-rolled beyond CSS for the callout spacing and the linked-line
tint.
Add cron, environment and run housekeeping logic for Actions
Pure modules behind the new Actions features, with tests:
- lib/cron.ts: a five-field POSIX cron reader (lists, ranges, steps,
month and day names, 7 for Sunday, the day-of-month OR day-of-week
rule) and the next N UTC fire times across several schedules, for the
"Scheduled runs" view. GitHub has no API for upcoming scheduled runs,
so they are worked out from the workflow file's on.schedule crons. - lib/environments.ts: REST mappers and request builders for
environments, deployments and their latest status, the status-to-
Pipelines state mapping, and reading the run id out of a status's
log_url/target_url so a deployment can link to its run. - lib/workflows.ts: enable/disable workflow and delete run/logs
request builders, and parseScheduleCrons. - lib/actionsHash.ts: an Environments tab (actTab=environments) and
actEnv for one environment's deployment history.
Actions: enable/disable, delete runs, scheduled runs and environments
Brings the Actions hub closer to native Pipelines:
- A workflow's "..." menu has Disable/Enable (PUT .../enable|disable)
and "Scheduled runs". The "enable it on GitHub" card on a disabled
workflow now has an in-place Enable button for writers. - Runs can be deleted, or just their logs, from the run page's "..."
menu and from each row of the runs tables (azure-devops-ui ColumnMore).
A confirm Dialog comes first; deleting a run from its page goes back to
the workflow's runs. Both are disabled while a run is still going,
as GitHub refuses them. - "Scheduled runs" opens a Panel with a Table of the next ten runs worked
out from the default branch's on.schedule crons, in local time. - A new Environments tab lists environments with their protection rules
and latest deployment; an environment opens its deployment history
(newest first, Load more), each linked to its workflow run when the
status points at one. Shimmer rows while loading (skeletonRows()).
All built from azure-devops-ui (Header command bar menus, ColumnMore,
Dialog, Panel, Table, TwoLineTableCell, MessageCard, ZeroData); nothing
hand-rolled. Mocks: CI gains two schedules, legacy stays disabled and
can be enabled, runs and logs can be deleted, and staging and
production have deployments; scenario noEnvironments shows none.
New shot pages: actions-wf-disabled, actions-envs, actions-env-staging.
Advanced Security: filter by ecosystem, package and secret validity
The request builders already sent Dependabot's ecosystem and package and
secret scanning's validity, but the filter bar never offered them.
Dependencies now has an Ecosystem dropdown (the values the REST filter
takes) and a Package keyword box; Secrets has a Validity dropdown
(active, inactive, unknown). They go to GitHub as query parameters, and
are kept in the hash as secEcosystem, secPackage and secValidity so a
reload or a shared link keeps them.
Built from the existing FilterBar items (DropdownFilterBarItem,
TextFilterBarItem); nothing hand-rolled. The mock secret list now
honours validity (it already honoured ecosystem and package).
Offer Semi-linear merge in the Complete panel
Native ADO's Complete dialog has "Semi-linear merge": rebase the source
onto the target, then create a merge commit. GitHub has no single call,
so it runs as two steps when the repository allows merge commits:
- If REST compare says the head is behind the target, update the PR
branch by rebase (the existing updatePullRequestBranch REBASE, with
expectedHeadOid), then poll REST GET /pulls/{n} (no HTTP cache) until
the head has moved and GitHub knows its mergeability, for up to 90 s. - Merge with REST PUT /pulls/{n}/merge, merge_method "merge" and
sha
set to the rebased head, so a push in between fails with 409 instead
of merging unrebased code.
The panel stays open and shows each step ("Rebasing...", "Waiting for
GitHub...", "Merging...") and keeps a clear error if it can't finish
(timeout, conflicts, 409, or a 405 such as required checks re-running on
the rebased commits); the store refreshes so a retry expects the new
head. The option is listed but disabled, with the reason shown under the
merge type, when the head is a fork without maintainer edits or the
branch refuses force pushes (classic allowsForcePushes false, or a
ruleset's non_fast_forward / update rule read over REST
/rules/branches/{branch}). It isn't offered for auto-complete, which
can't wait for the rebase.
Mocks: PR #317 (target moved on) and #318 (protected head), REST
compare/poll/merge and branch rules, scenarios rebaseConflict,
rebaseStuck and mergeBlocked. The dev harness gains ?click= so shots can
open panels; new shots pages pr-317-semilinear(-run/-fail) and
pr-318-protected.
Hand-rolled: the semi-linear history picture is another small SVG beside
the existing merge diagrams (no azure-devops-ui component draws one).
Add code search query planning, result mapping and its hash keys
The Files hub is getting a Code search view backed by REST GET /search/code
(text-match media type). This commit holds the pure parts, so they can be
tested without the network:
- lib/codeSearch.ts plans the requests within GitHub's limits: the whole
query stays within 256 characters, "All connected repositories" is split
into batches of repo: qualifiers (at most five per search), scope
qualifiers the user types are stripped so the page sets the scope, and a
query with no search term is refused before it costs a request. It maps
items to hits (fragments with merged match ranges, the indexed commit from
the contents URL), finds the hits in a whole file for the preview,
classifies failures (422 validation, 403/429 and secondary rate limits,
access, auth) and keeps a 10-a-minute request window in the page. - lib/searchHash.ts owns the hash keys search, searchAll, searchPath,
searchExt and searchHit, beside browseState.ts; browseState still sees the
repo and ref, and any BrowseState navigation drops them. - githubBrowse.searchCode makes the call; useHashExtras() exposes the state.
Add Code search to the Files hub
Native Repos has Code search: a search box in the top bar, results grouped
by file with the matching lines highlighted, filters, and a preview of the
file with the hits marked. This adds the GitHub-backed equivalent.
- Entry point: a "Search code" TextField at the end of the Files path bar.
Enter opens the view with search= added to the current Files address, so
Back (or the header's back button) returns to the same folder or file. - The view (CodeSearch.tsx): Header with a back button and Open on GitHub; a
FilterBar with the search text, a scope DropdownFilterBarItem (This
repository / All connected repositories, from the project's connections)
and Path and Extension TextFilterBarItems. The text fields use
onlyChangeOnEnter, so nothing is searched while typing: GitHub allows
about ten code searches a minute. - Results: a Table in the left pane of a Splitter, one row per file (icon,
name, match count Pill, repository and folder, up to four matching lines);
shimmer rows (skeletonRows) while it loads. Picking a row replaces the
hash's searchHit without a history entry. - Preview: the read-only Monaco viewer (new createSearchViewer in
monacoChunk) with every hit decorated and marked on the scrollbar, scrolled
to the first one, read at the commit the result was indexed at; Open file
goes to it in Files. - Limits and failures, all as MessageCards: sign-in needed; the rate limit
(from GitHub, or refused in the page before GitHub would refuse it, with
the time to try again); a 422 for the query; access. A 422 caused by one
repository GitHub can't search no longer sinks the others: the batch is
halved and retried while the minute's allowance lasts, and the card names
the repository. Searching another branch says the results are from the
default branch. Results are cached for five minutes. - Mocks: /search/code greps the fixture repos and answers with text_matches;
?scenario=searchRateLimit, searchSecondary and searchInvalid show the
failures. Shots pages: search, search-all, search-ratelimit,
search-invalid, and a live search page for the sandbox.
Hand-rolled: the matched text inside each result line is wrapped in spans
from the text_matches indices (no azure-devops-ui component highlights
ranges inside text). The one-column Table has no header row, so a little CSS
gives that column the row's width.
Add the dashboard widget model and hub links from non-hub frames
Groundwork for GitHub-backed equivalents of native's Pull Requests and Build
History dashboard widgets. lib/widgets.ts holds the pure parts: which widget
(or configuration) a frame is from its contribution id, the saved settings
(repo, PR filter, workflow) with safe fallbacks, the REST pull request mapping
and the all / created by me / assigned to me filter, and the run bars (oldest
left, height relative to the longest finished run so a run waiting hours for
an approval doesn't flatten the rest).
hubHref swapped the hub suffix of the current contribution id, which a widget
frame doesn't have. hubContributionId now swaps the relative id instead when
the current frame isn't a hub, so widgets link into our hubs with the same URL
building as every in-ADO link.
Add GitHub pull requests and GitHub Actions history dashboard widgets
Native ADO dashboards have Pull Requests and Build History widgets for Azure
Repos and Pipelines; these are the GitHub-backed equivalents, picked from the
widget catalogue like any other.
- GitHub pull requests: open PRs of a connected repo (all, created by me, or
assigned to me: a requested reviewer or assignee), with author persona,
title, number, age, target branch at 3 columns and up, and a Draft pill.
Sizes 2x2, 3x2, 4x2. Clicking opens the PR in our Pull requests hub. - GitHub Actions history: recent runs of one workflow or all of them as a bar
strip (height for duration, colour for result) with the last run's summary.
Sizes 2x1, 2x2, 3x2, 4x2; the bar count follows the width. Clicking a bar or
the summary opens the run in our Actions hub.
REST only (pulls list, /user for the filters, actions runs and workflows), so
they also work where GraphQL is refused. Each widget has its own configuration
contribution (repository, then filter or workflow); an unconfigured widget
shows the repo last open in the project, else the first connected one. Both
widgets and both configurations load dist/widget.html, which routes on the
contribution id and registers under the full and relative id. Signed out, a
widget says so and links to the Pull requests hub to sign in; it never opens
the sign-in popup itself. Links go through the host navigation service, as a
dashboard frame can't navigate the page.
The widget contracts are typed from azure-devops-extension-api/Dashboard, but
its helper classes ship as AMD modules esbuild can't bundle, so widget.tsx
restates the two values it needs (Success status, configurationChange event).
The interface method is reload; reconfigure is the same function too.
UI is azure-devops-ui: Table with TwoLineTableCell and shimmer rows,
VssPersona, Link, Pill, Status (RunStatusIcon), Tooltip, Spinner,
MessageCard, Dropdown and FormItem. Hand-rolled: the title is an h2 with
azure-devops-ui type classes (16px semibold, 10px padding: the native widget
title figures given with the brief; dashboards need a signed-in ADO session,
so not re-measured from here), and the history bars are plain anchors in a
flex row, because azure-devops-ui has no chart component.
Show the dashboard widgets in the mock harness and screenshots
npm run dev now serves widget.html?widget=prs|runs&size=…&repo=… (filter,
workflow, name and scenario too), with src/dev/widgetMain.tsx playing the
dashboard host: it sizes the frame to the dashboard grid, fetches the
registered widget object and calls load() with settings from the address.
&config=1 shows the configuration panel beside a preview frame that follows
each change, as the real configuration blade does.
Mocks: GET /user and the REST open pull request list built from the PR
fixtures (widgetMock.ts); workflow runs come from the existing Actions mock.
The mock SDK knows the widget contribution ids, and its navigate() goes back
to hub.html from a widget page. Shots pages: widget-prs, widget-prs-2x2,
widget-runs, widget-runs-4x2, widget-runs-2x1, widget-config and
widget-signed-out, plus live widget-prs and widget-runs on the sandbox.
Group PR reviewers into Required and Optional
Native ADO lists reviewers under "Required" and "Optional". GitHub's
nearest idea: when the target branch requires a code owner's review,
the code owners of the changed files are effectively required and
everyone else is optional.
The requirement comes from classic protection (baseRef
branchProtectionRule requiresCodeOwnerReviews, added to the actions
query) or a ruleset's pull_request rule (REST /rules/branches/{base}).
Who is a code owner comes from asCodeOwner on the review requests and,
because a request disappears once someone has reviewed, from the
CODEOWNERS file on the target branch (.github/, root, docs/) matched
against the changed files with GitHub's gitignore-style rules, last
match winning. Nothing is guessed: if the requirement is unknown, the
requests haven't loaded, or a reviewer could be covered by a team or
email owner, the section keeps its single "Requested" list. Bots are
never code owners. An empty group is left out.
Mocks: main carries a code-owner ruleset and the mock repo a CODEOWNERS
file, so #200 shows ann, carol and bob-dev as Required and dave and
Core Team as Optional; scenario noCodeOwners serves none.
Actions run page: Tests tab from JUnit and TRX artifacts
Native Pipelines runs have a Tests tab with a summary (total, passed,
failed, others, pass percentage, run duration) and the failed tests
grouped by file, with the error message and stack trace in a details
pane. GitHub has no test results API, but workflows commonly upload
JUnit XML as artifacts, so the tab reads those.
Only when the tab is opened, the run's artifacts are downloaded (likely
names such as test/junit/results first, then the smallest, up to 20 MB
in all; expired ones skipped), unzipped in the browser and every *.xml
or *.trx file that parses as JUnit or Visual Studio TRX is aggregated.
There is no zip dependency: lib/zip.ts reads the central directory
(zip64 sizes included) and inflates entries with DecompressionStream
("deflate-raw"), refusing any entry that inflates past its stated size.
Results are cached per run and artifact set.
lib/testResults.ts handles pytest (whose single "pytest" suite is split
by module), jest-junit (message and stack split out of the failure
body), Maven surefire ( fails, passed in the end),
nested suites, Node's built-in junit reporter (testcases straight under
, found on the sandbox's test/failing run) and TRX. The pass
percentage leaves "others" out, as native does. With nothing found, a
ZeroData explains that results come from JUnit artifacts and shows an
actions/upload-artifact step.
The run page's tab is now in the hash (actRunTab=tests|artifacts), as
native keeps it in the URL, so a link or a screenshot can open it.
Built from Card, TabBar/Tab with badge counts, Tree with
ExpandableTreeCell and ListSelection, Status, ZeroData, MessageCard and
the skeleton Table for loading. Hand-rolled: the row of summary figures
(azure-devops-ui has no statistic component) and the
blocks for
the message, stack trace and YAML example.Mocks: run 9001 gains a "test-results" artifact whose zip is built at
request time (two jest-junit matrix legs and a pytest report, deflated);
every mock artifact now answers a valid zip. Shots: run-9001-tests,
run-9004-tests (no JUnit), and run--tests in live mode.c11121c
Actions: Edit a workflow in the Files hub editor
Native Pipelines has "Edit" on a pipeline, which opens its YAML in an
editor. The workflow page now has an Edit button beside Run workflow
(for viewers with write access) that opens the workflow file on the
default branch in the Files hub with the editor already open.The Files hub learns to open straight into its editor from the hash:
edit=true (lib/editHash.ts, read through useHashExtras because
browseState drops unknown keys). Once the file has loaded it starts the
edit as the Edit button would and removes the key, so a reload or Back
does not reopen the editor; without write access it says so instead.Committing under .github/workflows/ needs the GitHub App's Workflows
permission. When GitHub refuses (its "withoutworkflowspermission"
message, or a 403/404 on a change that touches a workflow file, since
GraphQL's FORBIDDEN arrives without the message) the commit dialog now
says plainly that the app lacks the Workflows permission and suggests
committing on GitHub or with Git (lib/workflowWrite.ts). The app's
permissions are unchanged.Mocks: ?scenario=noWorkflowsPermission refuses a commit touching a
workflow file with GitHub's wording. Shot: file-edit-workflow.Nothing hand-rolled: a header command bar item and the existing editor.
1101481
Actions: Status badge dialog for a workflow
Native Pipelines has "Status badge" in a pipeline's menu, showing the
badge with its image URL and sample Markdown. The workflow page's "..."
menu now has it too: a Dialog with a branch Dropdown (default branch
first), GitHub's badge for that branch
(https://github.com/{owner}/{repo}/actions/workflows/{file}/badge.svg?branch=…),
and the image URL and Markdown in read-only TextFields with
ClipboardButtons. It says that a private repository's badge only shows
to people signed in to github.com with access, and says so again in
place of the image when GitHub does not send it.lib/workflows.statusBadge builds the URLs and Markdown (encoded branch
and file names, escaped alt text), with tests.Hand-rolled: the
for the badge itself (azure-devops-ui has no
image component).5a64bd0
Add a Forks page and pull requests from forks
Native Repos lists a repository's forks, creates one, and opens a pull
request from one. All of it is REST here.Forks page (Branches hub, "..." > Forks, hash view=forks): a Table of
the forks the viewer can see (GET /repos/{o}/{r}/forks, newest first),
with owner, last push, and behind / ahead of the parent's default
branch from the cross-repository comparebase...owner:branch(loaded
a few at a time, like Branches, and shimmer rows while the list loads).
Each row links to GitHub and offers "New pull request" from the fork."Fork" opens a confirm Dialog that says it creates a new repository on
GitHub under your account or an organisation you pick (from /user/orgs),
with "Copy the default branch only", then POST /repos/{o}/{r}/forks.
A GitHub App user token often can't create repositories on an account,
so a refusal says so plainly and links GitHub's own fork page.New pull request: when the repository has forks, a source repository
Dropdown sits before the source branch (kept in the hash as
sourceRepo). For a fork, the branch list is the fork's, the comparison
is REST compare with the owner:branch head plus REST pulls?head= for an
existing pull request, the preview compares owner:branch, and the pull
request is created with createPullRequest's headRepositoryId. Swap is
off while the source is a fork.Mocks: three forks with branches, compares and a create (scenarios
forkDenied, noForks); shots pages forks, forks-dialog and newpr-fork.87fe8bc
Say which pages need GraphQL in cloud live mode
CLAUDE.md listed only the PR overview and Releases as broken in cloud live
mode. A pass over every page with --live after the gap work shows the
Branches and Tags lists, Releases, the PR list and all PR pages fail on the
egress proxy's GraphQL refusal, while Commits now degrades (REST list, empty
PR/status columns) and the new features (code search, branch policies, Tests
tab, widgets) work. It also records that the refusal can't be configured
away, and asks new features to prefer REST so they can be checked in cloud.4698581
Fix the Actions Environments note in CLAUDE.md
The previous wording ran the Environments exception into the list of
working Actions pages.3cf48a1
Read branch rules for the policies page through lib/branchRules
The Branch policies page fetched GET /rules/branches/{branch} with its own
inline request, beside the branchRules.ts reader the PR page uses. Both
now build the path with branchRulesRequest and parse with toBranchRules, and
BranchRule carries the ruleset id and source the policies page needs, so the
editing work that follows has one rules reader to build on.cb7677d
Actions Library: variables and secrets requests, validation and errors
The Library tab (GitHub's equivalent of Pipelines > Library) needs the
REST calls for repository and environment variables and secrets, GitHub's
naming rules (letters, digits, underscore; not a leading digit or
GITHUB_; unique case-insensitively), the 48 KB value limit, and words for
the 403/404 GitHub gives when the app lacks the Variables or Secrets
permission. All pure, in lib/library.ts, with tests. actionsHash gains
actTab=library.Secrets are listed and deleted only: setting one needs a libsodium sealed
box and we add no crypto, so the UI links to GitHub's settings instead.5c367da
Actions: Library tab for variables and secrets
A Library tab beside Environments shows one variable group per scope, the
repository and then each environment, as native Library's groups: a
collapsible Card per group (Library icon, counts in the description) with
a Table of Name, Value and Last updated. Secrets show ******** with a lock,
as native's locked secrets. Admins get "Add variable" and "Set secret on
GitHub" in the card header and a row menu (Edit, Delete; Set on GitHub,
Delete for secrets); clicking a variable row edits it. Add and edit use a
Panel with TextFields for name and value, validated before sending;
delete confirms in a Dialog. Others see the tables only: GitHub's own
variables and secrets settings are admin-only.Variables and secrets fail separately, each with its own MessageCard
naming the missing app permission, so the rest of the tab keeps working.Only CSS added: spacing for the stacked cards and messages, and a
flex-grow on the collapsible card header, which azure-devops-ui wraps in a
plain flex-row so its command bar otherwise sits beside the title.Mocks keep variables and secrets in memory for the session; scenarios
noVariablesPermission, noSecretsPermission, notAdmin and manyVariables
(paging). Shots: actions-library(-noperm, -readonly, -edit, -add) and a
live actions-library page. In cloud live mode the egress proxy refuses
these paths, noted in CLAUDE.md.4e26bf2
Let repository admins edit branch policies, saved as the extension's own ruleset
Native ADO lets an admin turn branch policies on and set their values on the
Branch policies page. For a viewer with permissions.admin the page is now
editable: minimum reviewers (count, reset votes on push, last-push approval),
comment resolution, limit merge types (pull_request.allowed_merge_methods),
build validation (add and remove required status checks, "up to date"
strict mode), code owners, block force pushes, restrict deletions and signed
commits.Everything is written to one repository ruleset per branch that the extension
owns, "Azure DevOps policies: " (POST /rulesets the first time, PUT
/rulesets/{id} after, DELETE when nothing is left in it). Rulesets and classic
protection the extension didn't create are never written: they are listed
read-only under "Rules from GitHub" with links to edit them on GitHub, and a
note explains that GitHub enforces the union. A PUT carries over rules and
parameters the form doesn't know (the lock, integration ids, Copilot review)
and leaves enforcement and bypass actors alone.Save and Discard in the header rather than native's save-on-change: one
ruleset holds every policy, so each toggle or keystroke in the reviewer count
would be a separate PUT, a half-typed status check would be sent, and a
refused write would leave the page out of step with GitHub. Batching also
lets the page show which policies a Save changed.403/404 on a write shows "You need admin rights on this repository" with
GitHub's words (a GitHub App without Administration answers 403); 422 shows
GitHub's message. The logic (rules to form and back, the diff, the request
to send, the error wording) is pure in lib/policyEdit.ts with tests. Mocks
keep rulesets in memory for the session; scenarios rulesetsDenied and
rulesetsInvalid; shots branch-policies-edit, -saved, -denied and -locked.Built from azure-devops-ui Toggle, TextField, FormItem, Checkbox,
RadioButtonGroup, Table, Button, Card, Header and MessageCard; nothing
hand-rolled.ae2cd52
Add Lock and Unlock to the branch menu, with a lock icon on locked branches
Native ADO's branch "..." menu has Lock, after which nobody can push. Admins
now get Lock / Unlock on each branch row. Lock adds theupdaterule to the
extension's own ruleset for the branch (creating it when there is none);
Unlock removes it, deleting the ruleset if the lock was all it held. The
ruleset is read fresh first so its policies are kept. Non-admins don't see
the items.A locked branch shows a Lock icon in its row (tooltip "Locked: nobody can
push to this branch") in place of the Shield used for branches with
policies, so the two read differently. The locked set comes from the
repository's rulesets (GET /rulesets, then each extension-owned one), so
readers see the lock too. Errors use the same wording as the policies page.e1219f2
Stop the policy editor's status check field growing 360px tall
The "Add status check" TextField had flex: 0 1 360px, and azure-devops-ui
wraps it in a column, so the basis set its height instead of its width and
Build Validation showed an empty 360px box with no visible placeholder.
Give it a width instead.b2926d6
Build cherry-pick and revert from REST Git data and the merges API
Native ADO offers Cherry-pick and Revert on a commit and on a completed
pull request; both put the change on a new topic branch made from a
target branch. GitHub has no cherry-pick or revert API, so
lib/cherryPick.ts plans it and runPick runs it over REST:
- X = the commit itself for a plain cherry-pick, otherwise a synthetic
commit with the "to" tree on top of the "from" commit; - S = a commit with the target head's tree and parent "from", and a
tmp/ branch created at S (created, never force-updated); - POST /merges (base = tmp branch, head = X) does the three-way merge
with merge base "from"; 409 is reported as a conflict to resolve
locally (GitHub doesn't say which files clash); - the merged tree is recommitted on the target head with git's own
message (and the original author for a cherry-pick), the topic branch
is created there, and the tmp branch is deleted in a finally.
A revert can't merge C's parent into a branch whose parent is C, as the
brief first suggested: that parent is already an ancestor, so GitHub
answers 204 "nothing to merge". The synthetic commit gives the inverse
change with merge base C instead. Merge commits need a mainline parent,
like git cherry-pick -m. Nothing writes to an existing branch: the only
DELETE refuses any ref outside tmp/.
githubBrowse.ts adds getGitCommit, getCanPush (REST permissions.push),
runCherryPick and getPrPickSource, which works out how a merged pull
request landed (merge commit: mainline 1; squash; rebase: walks back one
first parent per PR commit) since REST doesn't name the merge method.
466e6a4
Add Cherry-pick and Revert dialogs to commits and completed pull requests
The commit list's row menu and the commit page's menu gain Cherry-pick
and Revert; a completed pull request's menu gains Cherry-pick beside the
existing Revert (which stays GitHub's own revertPullRequest). Each opens
an azure-devops-ui Dialog like native's: a target branch Dropdown
(defaulting to the branch being browsed, or the pull request's target),
a Mainline parent Dropdown for merge commits, and a topic branch
TextField prefilled as -cherry-pick- (or -revert-, or
-pr-) and checked like New branch. While it runs the dialog shows the
step with a Spinner; when done it offers "Create pull request" (our New
pull request page, source = topic branch, target = target) and "View
branch". Failures, including a conflict, show in an error MessageCard.
Nothing is hand-rolled.
The items are disabled for a viewer without push access (REST
permissions.push, so it works where GraphQL is refused), and the dialog
says why if it is opened anyway.
The dev harness answers the whole sequence (src/dev/cherryPickMock.ts,
ahead of the Delete-folder Git data mocks, claiming only its own trees,
commits and tmp/ branches), with ?scenario=cherryPickConflict for a 409
from the merge. Shots: cherry-pick-dialog (a merge commit, with the
mainline picker), cherry-pick-done, cherry-pick-conflict, revert-dialog
and pr-cherry-pick. The list's row menu is used for most of them because
the commit page's diff doesn't load inside the shot's time budget for
every click step.
a2db670
Word Delete folder's stale-branch refusal for a folder
Delete folder reused the file editor's STALE_TEXT, which says the branch
changed "since you opened this file". A folder delete has no edits to
lose, so it now has its own sentence that names the folder and says to
refresh or commit to a new branch.
464f37d
Ask before leaving Branch policies with unsaved edits
The policy editor kept its form in component state only, so Back, the
repo switcher or back/forward threw away unsaved changes without a word.
It now registers with hostNav's edit guard and shows the same Discard
dialog as the release form, and warns on a closing tab.
The page's address (view=policies, polBranch) is dropped by
browseState.parse, so the Back target parses the same as the page itself
and decideNav would have allowed it. decideNav takes an anyNav flag for
such pages: while dirty, every navigation asks.
777dbb2
Read a long pull request's last commit before cherry-picking it
getPrPickSource only fetched the PR's commit list when it had 100 commits
or fewer, and then took the last entry of page 1. A rebase-merged PR with
more than 100 commits therefore had no "last commit" to compare, was read
as a squash, and Cherry-pick quietly applied only its final commit. The
MAX_REBASE_WALK refusal meant for that case could never fire.
It now asks for the page that holds the last commit (GitHub lists up to
250), so a long rebase merge is recognised and refused with its message.
9fd5492
Semi-linear merge: merge only the head the rebase made
After the rebase, the wait took any head that had moved off the old one
as the rebased head and merged it. Someone pushing to the source branch
in the seconds between the rebase and the poll would have had their
unreviewed commits merged, despite the header's promise that a push in
between is refused.
updatePullRequestBranch answers the PR's headRefOid. When that is the
new, rebased head, the wait now requires exactly it and stops with
"changed after it was rebased" for anything else; when GitHub hasn't
reported it yet, the old behaviour stands. The mock's mutation now
answers the rebased head, so the harness exercises the new path.
72a36dd
Show a branch's lock when it arrives after the rows
The Branches tree sometimes never showed the lock icon (or the Unlock
menu item) on release/2.0: the lock read finishes after the branch rows
have rendered, and azure-devops-ui's Tree does not re-render its rows
for new columns alone, only for a new item provider. The provider is now
memoised on the columns too, as CLAUDE.md asks for any state a cell
reads. Found while making the branches-locked shot stable: 2 of 4 headless
runs lost the lock before; 5 of 5 show it now.
c745fa5
Wait for the page to settle before each mock ?click= step
Click-driven shots flaked when a click landed before the data did. The
runner now waits (up to 8 s per step) for the target to be present and
enabled and for the page to be quiet for 300 ms: no fetch in flight and
no shimmer or spinner on screen (a lazy diff below the fold keeps its
spinner, so off-screen ones don't count). Shots with ?click= get a
20 s virtual-time budget to cover the waits, and branches-locked drops
its four dummy "Branches" clicks that were only there to stall.
cherry-pick-dialog: the dialog was missing in 1 of 3 runs before (the
budget ran out with the menu open); 5 of 5 and 3 of 3 shots runs now
show it. branches-locked: stable once the lock rendering fix landed.
d1a6dde
Don't cache a failed permission read as 'no access'
getIsRepoAdmin and getCanPush turned a failed read into false inside the
30-second cache, so a viewer who had just signed in (or hit a blip) saw
Cherry-pick, Lock and the admin items disabled for up to half a minute.
The cache already drops rejected reads; map the failure to false outside it
instead, so the next render asks again.
651084a
Lay out the Tests tab's summary figures like native
Measured on a public native build's Tests tab (dnceng-public): each
figure is 36px/600 with a 42px line height in the secondary text
colour, with its 14px label underneath. Ours put the label on top and
the figure at 21px/600/28px in the primary colour. The figures now sit
above their labels at the native size and colour; the status icons stay
beside Passed, Failed and Others since we have no donut charts. Still
hand-rolled (a flex row of spans): azure-devops-ui has no statistic
component.
88e48cb
Open Clone in a Panel like native Repos
Native Repos' Clone opens a 480px Panel, "Clone Repository", not a
callout: measured on dnceng-public's public wiki repo, the header title
is 21px/600 with 16/16/16/20 padding, the content has 20px sides, the
"Command line" row has HTTPS and SSH as buttons (the chosen one
primary) 4px before a read-only URL field and its copy button, "Learn
more about HTTPS" sits 16px below, and the "IDE" form item 24px below
that. Ours was a 400px callout with a 14px/600 title, compact protocol
tabs and a 12px rhythm.
Now a Panel with the same pieces. Measured after, against native (y in
the panel): Command line label 86/86, protocol buttons 113/113 (73 and
55px wide, both), URL field 114/114, Learn more 161/161, IDE label
202/202, Clone in VS Code 227/227. Native's VS Code button is a split
button listing other IDEs; ours stays a plain Button with one target.
Adds a files-clone shot.
7bd4b03
Use native's measured header spacing on Forks, Branch policies and Code search
Native Repos list headers (Branches, Tags, Pull requests on
dnceng-public) have 24px top padding, none below, and a 28px title
row, so the tabs or first card start 52px below the top of the page;
Actions already does this with .gh-act-header. These three new pages
used the older .gh-hub-header box (22px top, 34px title line, 16px
bottom, fixed 72px), which put content 20px lower than native and
clipped Code search's subtitle ("repo, default branch") out of sight.
Measured before / after (header height, first content y):
- Forks: 72 / 124 -> 52 / 104 (native 52, tabs or card straight after)
- Branch policies: 72 / 124 -> 58 / 110 (title row has the branch icon)
- Code search: 72 (subtitle clipped) / 124 -> 69 / 121, subtitle shown
Title: 21px/600 with a 34px line -> 28px line (native 21px/600, 28px).
The older pages that share .gh-hub-header (Branches, Tags, Pull
requests, Releases, Security, Compare) still use the 72px box; left
alone here as outside this audit.
01152f5
Space Branch policies cards 16px apart, as native
Each policy card and message card had a 16px bottom margin on top of
the page's 16px card-to-card margin, so they sat 32px apart. Native
cards (and a message card above a card) are 16px apart, measured on
dnceng-public's build Tests tab: message card bottom 265, next block
281. Now 16px: message card 110-176 then Branch Policies at 192
(was 208); Branch Policies card bottom 476 then Build Validation at
492 (was 508). Also updates the header CSS comment to say the new Repos
pages (previous commit) use the measured Pipelines header spacing.
a874d7f
Match native widget titles: 16px regular, 14px side padding
Measured on a public dnceng-public dashboard (the Build history and
Build Health widgets): a widget title is an h2 at 16px/400, 10px from
the top and 14px from the sides; the build history body sits 14px in.
Ours, from figures given without a measurement, was 16px/600 with 10px
sides. Now 16px/400 (font-weight-normal, since an h2 is semibold by
default) with 14px sides on the title, subtitle and body, and the
scrolling PR list bleeds 14px to the widget edges to match.
c421565
Use the measured native header on the older list pages
The fidelity audit measured native Repos and Pipelines list headers at
padding 24/20/0/20 with a 28px title row (content starts 52px down), and
moved the new pages onto .gh-act-header. Branches, Tags & Releases, Pull
requests, Security and Compare still used the older 72px .gh-hub-header box,
so their content sat about 20px lower than native. Give them the same class.
4de7132
Add a live-check list for the gap-analysis features
The features are verified with tests, the mock harness and measurements
against public native pages, but the GitHub writes (cherry-pick, rulesets,
semi-linear merge), GraphQL pages and everything inside real Azure DevOps
can't be checked from a cloud session. This lists exactly what to check on
a compliant device, including the GitHub App permissions to confirm first
and the sandbox clean-up.
65353f2
Add the maths for comparing two versions of an image
The image diff modes (2-up, Swipe, Onion skin) need fit-to-width scaling
with one scale for both sides, a frame the size of the larger image,
"W × H px · size" labels, a validated stored mode and a clamped slider
value. Keeping these pure in src/lib means they are tested without a
browser; an SVG that reports no natural size falls back to 300 × 150.
ef95168
Compare modified images in 2-up, Swipe or Onion skin
Reviewers asked for GitHub's image comparison: a modified image (raster
or SVG) in the PR Files tab, commit detail, PR Updates and the new PR
page (all share FileCard) now opens on 2-up, with Swipe and Onion skin
beside it. 2-up shows both sides at one scale, so a size change is
visible, each labelled with its pixel size and file size over a
checkerboard. Swipe and Onion skin stack both in one frame the size of
the larger image; Swipe was cheap with the same frame, so it is in.
Added and deleted images keep the single before/after view.
The switcher is a ButtonGroup of Buttons with aria-pressed, the same as
the Source / Rendered toggle, wrapped in a labelled role=group div
because ButtonGroup takes no aria-label. The chosen mode is per user in
the prefs document (imageDiffMode) and applies to every image on the
page at once.
Hand-rolled: the opacity and swipe sliders are native
elements, because azure-devops-ui has no slider;
they carry a label, aria-valuetext and a visible value, and take the
theme's primary colour through accent-color. The stacked frame is
plain positioned divs, as no component draws overlapping images.
The checkerboard now uses neutral-100 at 8% instead of neutral-8 at 6%:
neutral-8 sits close to the page colour in both ADO themes, so it hid
the transparency it was there to show.
cad720b
Give the mock PR images worth comparing, and shots of each mode
PR #200's logo.png was two 48 × 24 opaque pictures and its SVG kept the
same size, so the comparison modes had nothing to show. The head logo
is now 200 × 120 with a transparent background over a 160 × 96 opaque
base, and the head SVG is 240 × 120 with a translucent badge. New shots
pages pr-image-2up, pr-image-onion and pr-image-swipe (the last on the
SVG) check each mode in both themes.
e9625fb
Show GitHub markdown alerts as azure-devops-ui message bars
GitHub renders > [!NOTE], [!TIP], [!IMPORTANT], [!WARNING] and [!CAUTION]
as div.markdown-alert with an octicon in the title. The sanitiser drops the SVG,
so READMEs, markdown previews, PR descriptions, comments and release notes showed
each alert as a bare "Note" line.
prepareMarkdownHtml now dresses each alert in MessageBar's own markup after the
class filter: bolt-messagebar with severity-info/success/warning/error, the Fluent
icon span (Info, Lightbulb, ChatWarning, Warning, ErrorBadge) and the message
column, so the library's CSS supplies the palette colours in both themes. The
rendered HTML is a string, so no React MessageCard can go in; this reproduces its
look with its classes instead (hand-rolled markup, library classes). MessageBar
has no purple, so IMPORTANT is info-blue with the speech-bubble icon. A few lines
of browser.css give MessageCard's corners and padding and let the message hold
several blocks under the title. README HTML still cannot borrow the bolt classes.
The mock README carries all five types; new shot page file-readme.
23e6847
Draw mermaid diagrams in rendered markdown
GitHub renders ```mermaid blocks client-side, so its markdown API and bodyHTML
hand us only the source (inside a js-render-needs-enrichment section, or as a
highlight-source-mermaid / language-mermaid code block) and our pages showed
code. prepareMarkdownHtml now normalises each of those shapes to a
markdown-mermaid block holding the source, set after the class filter so README
HTML cannot ask for a diagram itself. The Markdown component (README, Preview,
rendered PR diffs, PR description, comments, release notes, advisories) draws
any such block with mermaid and redraws on a theme change.
mermaid 11.17.2 is pinned exactly and loaded by dynamic import from
src/mermaidChunk.ts only when a page holds a diagram; its diagram types split
into chunks of their own. Production initial bundle (index.js and the chunks it
imports statically): 1,328,543 to 1,329,076 bytes. dist grows from 9.6 MB to
13.0 MB, all lazy. securityLevel "strict", htmlLabels off (labels are SVG text,
not HTML in a foreignObject) and the SVG goes through DOMPurify's SVG profile
again before it is inserted. Theme "neutral" in light and "dark" in dark, in
the page's font at 14px. If the chunk or the render fails, the source code
block stays. The diagram container is a plain div (role img): there is no
azure-devops-ui component for an SVG drawing.
Mocks: a flowchart in the README and docs/guide.md (with a broken diagram for
the fallback), a sequence diagram in PR #200's description; shot page
file-guide-mermaid. Licence added to THIRD-PARTY-NOTICES.md.
1f19a7b
Expand hidden context in diffs: 20 more above, 20 more below, or all
Monaco's hideUnchangedRegions put its controls in thin click-or-drag strips
above and below an "N hidden lines" label that only a double-click opened, so
nobody found them. Native ADO and GitHub show an expand row instead. The diff
now collapses unchanged regions itself (setHiddenAreas with its own source, so
folding still merges) and puts a row in each gap: a run of 20 lines or fewer
opens whole with one click on the row; a longer one offers "Show 20 more
above", "Show 20 more below" (only the directions that exist at the top and
end of the file) and "Show all N lines". The file card gets a "..." menu with
"Expand all lines" / "Collapse unchanged lines". It works inline and side by
side (the original editor gets a spacer of the same height, so the two panes
stay aligned), in PR Files, commit detail, PR updates and compare, which all
share DiffBody.
The region maths (gaps between Monaco's line changes, three context lines,
reveal state) is in src/lib/contextRegions.ts with tests. The row is subtle
azure-devops-ui Buttons in a view zone (src/ContextRow.tsx); the "..." is a
MoreButton. Nothing hand-rolled beyond the row's container div. The zones set
showInHiddenAreas, because Monaco hides a zone whose next line is hidden.
The mock harness's ?click= also takes scroll:, for shots of content
below the fold (scrollIntoView inside a Monaco zone scrolls Monaco's own
layers instead of the page). New shots: pr-diff-expand, -sbs, -more.
0edd259
Show CSV and TSV files as tables, and table diffs in PRs and commits
GitHub shows a .csv or .tsv as a table; we showed only the text. In Files a
.csv/.tsv now gets the Preview tab markdown has (and opens on it): an
azure-devops-ui Table with the first record as its header, a row-number
column, ColumnFill, selectable text and the Table's own virtualisation. At most
5,000 rows are shown, with an Info MessageCard saying how many there are.
In PR and commit diffs a .csv/.tsv gets the Source/Rendered toggle markdown
has, opening on Source. Rendered is a table diff: an LCS on whole rows, then in
each run of edits a removed row pairs with a nearby added row sharing at least
half its cells as "changed" (so an edited record pairs with its new self, not
whatever sits at its position). Added and removed rows take the MessageBar
status backgrounds through a custom renderRow (TableRow with a class), edited
cells the warning background with the old value struck through. The header row
appears in the body only when it changed.
The parser (lib/csv.ts) is RFC 4180 and lenient: quoted fields with
delimiters, line breaks and doubled quotes, CRLF/LF/CR, BOM, blank lines and
ragged rows; tests cover it and the row diff. Nothing hand-rolled beyond the
/ pair inside an edited cell. The rich-diff files the image-mode work
touches (PrRichDiff.tsx, lib/richDiff.ts) are unchanged: the CSV branch sits in
PrFiles.tsx and the new CsvTable.tsx. The fileTabs parameter is renamed
hasPreview (same behaviour).
Mocks: data/sales.csv (quoted commas, a quote, a line break), data/scores.tsv
(6,200 rows, for the cap note), a modified data/prices.csv in PR #200. Shot
pages file-csv, file-tsv-big and pr-200-csv. Initial bundle +7.5 KB.
210eadd
Add hover cards for people, pull requests and work items
GitHub and ADO both show a small card when you rest on a person; the
extension showed at most a one-line tooltip. HoverCard opens an
azure-devops-ui Callout after 500 ms on a resting pointer or on keyboard
focus, stays open while the pointer is on the card, closes on leave
(with a 300 ms grace to cross the gap) or Escape, and only one is ever
open. The timing is a pure state machine in lib/hoverCard.ts, tested
with the REST field mapping.
- People: GET /users/{login} (cached per session) for name, bio,
company and location, plus "Member of {org}" from
GET /orgs/{org}/members/{login}. The role needs
/orgs/{org}/memberships, which asks more of the app, so it is left out. - Pull requests: GET /pulls/{n} and /reviews: ADO's state pill, author,
source and target branches, created/updated, review counts. - Work items: one ADO workitems call with AssignedTo, AreaPath and
IterationPath, and the type icon already loaded per project.
Cards fetch on open, show core.css shimmer lines while loading and a
quiet line on failure. All REST, so they work in cloud live mode.
azure-devops-ui's IdentityCard can't be reused: it resolves AAD
identities through ADO's identity service and its GitHubCard prints a
hard-coded login. The person card is Callout + VssPersona + Link + Icon
laid out with the IdentityCard's own CSS classes (360 px card, 72 px
persona, title-s name, contact-line rows). Hand-rolled: the wrapper
span that acts as the hover anchor (no component offers delayed,
interactive hover; TooltipEx is pointer-transparent and says not to be
used for custom callouts), the card body divs, and the shimmer divs.
VssPersona always adds a name tooltip, so tooltips are hidden while the
pointer is on an anchor.
Mocks: hoverMock.ts answers the profile, membership and pull request
calls (cherry-pick's merged-PR answer now spreads the same fields), the
work item mock returns the extra fields, ?scenario=hoverFails fails the
lookups, and ?click=hover: rests the pointer for screenshots.
952bcbc
Show hover cards wherever a person, PR number or AB# pill appears
Wires the cards in: PersonLine (commits, commit detail, branches,
Actions "Triggered by"), the PR list and dashboard widget avatars, the
PR page's author, reviewers and assignees, pushes, and the run page's
actor; PrCell (commits and branches PR columns); the AB# pills and
links on commits, replacing their one-line tooltip. RepoHubPage and the
PR page provide the repo through HoverRepoContext so a bare PR number
can be looked up.
Rendered markdown is GitHub's HTML, so its links can't be wrapped:
PrLinkHovers listens on the markdown box and gives links to a pull
request in the same repo (#123 once GitHub has linked it) the PR card.
Links to other repos get none.
New shots pages: hover-person, hover-pr, hover-pr-merged,
hover-workitem, hover-pr-author, hover-reviewer, hover-fails.
96d99b1
Let live mode read public profiles for the hover cards
The person card reads GET /users/{login}; the live guard refused it, so
the card could never be checked against real data locally. Public
profile reads are now allowed (exact /users/{login} only, not its
repos, events or other sub-paths; writes stay refused), with tests.
In cloud sessions the egress proxy still refuses /users/{login} and
/orgs/{org}/members/{login}, so there the card degrades to the name and
avatar with a quiet error line; CLAUDE.md says so.
ab9e775
Comment on a range of lines, and show range threads as ranges
Threads could already be started on a range by dragging or Shift-clicking the
gutter "+" (addPullRequestReviewThread already sent startLine/startSide with
line/side), but nothing showed a range once it existed and there was no way
from a text selection. Now:
- Selecting text across several commentable lines brings the "+" up beside
the last line ("Comment on lines 16–18"); pressing it opens one comment box
for the whole range. A press in the gutter keeps its own path. - Existing multi-line threads mark their lines with a bar at the gutter's code
edge and a light tint, on the side they belong to; the thread stays anchored
under the last line. In the diff its card says "Lines 13–15" (native ADO's
wording); on the Overview the file link reads "path, lines 13–15", and the
comment box header uses the same label. - Suggestions on a range already replace the whole range (parseSuggestions
takes the thread's startLine), so the composer's suggestion button, which
is given the range's original lines, needed no change.
Fix found on the way: Monaco hides a view zone whose next line is hidden, so
since unchanged regions are collapsed with setHiddenAreas a thread or comment
box on the last line above a collapsed run vanished. Every zone now sets
showInHiddenAreas.
Pure logic (threadRanges, linesLabel) is in src/lib/inlineThreads.ts with
tests. Mocks: PR #200 gets a thread on lines 13-15 of src/lib/names.ts; the
mock create mutation already accepted startLine. The harness's ?click= gains
pointer:/shiftpointer:@, which presses on a Monaco line
number, for the pr-range-compose shot (press 16, Shift-press 18); pr-range-thread
shows the existing range thread. Nothing hand-rolled beyond the existing
gutter button and a span for the "Lines" label.
9fd4dca
Release 0.39.0
Brings in the gap-analysis work: native-feeling ADO equivalents for the
GitHub features the extension didn't expose yet (Files clone, line links,
code search, folder delete; Commits filters, AB# links, cherry-pick and
revert; branch policies view, editing and lock; forks; semi-linear merge;
Actions environments, Library, Tests tab, schedules; dashboard widgets),
plus image diff modes, markdown alerts, Mermaid, CSV tables, expandable
diff context, range comments and hover cards.
14e3990
Full changelog: v0.38.0...v0.39.0