Repository navigation
Releases
v0.44.1
What's changed
Save reads within a quarter second, and before the hub's frame goes
In 0.44.0 the PR list came back at once after a hub switch but Tags
didn't. A save waited for the cache to be quiet for a second, and every
fetch or hover card restarted the wait; ADO tears a hub's frame down the
moment you switch hubs, so on a slower page (Tags reads tags, branches
and releases over GraphQL) leaving soon after the rows arrived meant the
save never happened. Reproduced in the mock: leave Tags 50 ms after its
rows appear, switch back, and 0.44.0 shows shimmer.
- A change is written at most 250 ms later with the latest state, and
later changes no longer put the write off. - A pending write goes at once when the frame is hidden or unloaded
(visibilitychange, pagehide). - Each page logs "[query cache] restored N saved reads" once, so a live
check can see whether a hub started from its saved reads.
Release 0.44.1
Hubs that settle more slowly than the PR list, such as Tags, now come
back at once after a hub switch too: saved reads are written within a
quarter second, and before the hub's frame is torn down.
Full changelog: v0.44.0...v0.44.1
v0.44.0
What's changed
Read the PR list and Commits through TanStack Query
Lists felt sluggish because every load hook dropped back to shimmer rows
whenever it ran again: coming back from a PR, switching tab, paging,
changing a filter or pressing Try again all blanked the list, even when
the rows were in the 30 s cache.
This adds @tanstack/react-query (v4: v5 needs React 18, and
azure-devops-ui pins React 16) with one QueryClient per page, and moves
the two lists that flash most onto it:
- Pull requests: useQuery under prListKey. Back from a PR, or back to a
tab, shows the rows at once and refreshes them in the background.
A new page or filter keeps the current rows until its own arrive; the
pager waits for the page it last asked for. - Commits: useInfiniteQuery, a page per "Load more". A filter change
keeps the rows; another branch starts from shimmer rows, so one
branch's commits never show under another's name. A failed refresh or
"Load more" keeps what is shown (Load more says so in a MessageCard).
Fresh for 30 s as before, kept 10 minutes, refetched on focus when stale.
Only a network or 5xx failure is retried, once; sign-in, permission and
rate-limit errors show at once as before. invalidatePrs and
invalidateRefs now also refetch what is on screen and drop the rest
(dropQueries), so a merged PR is not shown under Active afterwards;
sign-out clears the whole cache. listPrs and listCommitsRest no longer
cache themselves.
No hand-rolled markup: the existing Table, Card, ZeroData and
MessageCard are reused.
Add the shared query hook, key roots and IndexedDB persistence
useGh wraps useQuery in the loading/error/ready shape the hubs already
render, so each hub converts the same way: loading only while there is
nothing to show, an error only while there is still nothing to show.
Every query key starts with a root (prs, pr, refs, browse, actions,
security, misc), and invalidateRefs, invalidateBrowse, invalidatePr,
invalidatePrPage and invalidateForks now drop their root too.
queryPersist.tsx keeps opted-in reads (meta.persist) in IndexedDB, one
record per organisation, ADO user and hub, so reopening a hub shows its
last lists at once while they refresh. Only lists and metadata opt in;
records last a day, a new extension version drops them, and Sign out
clears them all. Signing in refetches every read, so sign-in state is
no longer part of the keys (it made a restored cache miss on load).
The PR list and Commits opt in.
Test the query persister; keep the mock's scenarios apart
Unit tests (fake-indexeddb) for idbPersister: one debounced write a
second after the last change, Dates restored intact, one record per
id, and Sign out clearing every hub's record; and for shouldPersist,
which keeps only successful reads that opted in.
In the mock harness the user id now carries the scenario (and live
mode), so a list saved under "ok" can't hide a failure scenario's
error, and Reset storage deletes the saved reads too.
Read Pushes, Branches, Tags, Releases and commits through queries
The ref hubs dropped to shimmer rows on every revisit, Try again and
write. Their reads now go through useGh under the "refs" root, so they
show what they had while refreshing, and invalidateRefs (Try again, and
every branch or tag write via useRefWrites) refetches what is on screen.
- Pushes: useInfiniteQuery, a page per "Load more"; a failed page keeps
the rows and says so in a MessageCard instead of replacing the list. - Branches, Tags, Releases, policies, the compare pickers' names, the
commit header, the viewer and admin flags are kept between visits.
Compare results, commit file patches, locked branches and the policy
editor's own ruleset are not (they go stale or must start fresh). - invalidateReleases drops ["refs", "releases"] too, since the release
caches are not among the ref caches. - Reload counters and signedIn are gone from the loaders: sign-in now
refetches everything.
No hand-rolled markup: the new warning is a MessageCard.
Read the Files hub through queries
Files re-fetched the tree, folder, last changes and README on every
visit, and dropped to shimmer on Try again. Its reads now go through
useGh under the "browse" root (forks, the default branch and the latest
release under "refs"), so going back up a folder or reopening the hub
shows what it had at once.
- Kept between visits: repo info, the project's connected repos (per
project), the default branch, the latest release, folder listings,
last change per entry and History. Never trees, file contents, the
README, markdown source, blame or compare. - The tree, a file, the README, blame and compare are fresh for five
minutes, so refocusing the frame doesn't re-download a large tree or
file (they were never refreshed during a visit before). - The view reads the tree through the cache, so after a commit (which
drops "browse") it reads the new tree. - History's "Load more" is a useInfiniteQuery; a failed page keeps the
rows and says so in a MessageCard. - Compare builds Monaco from the query's stable value, so a background
refresh does not rebuild the editor. - Opening a file with edit=true waits for the write context.
No hand-rolled markup: the new warning is a MessageCard.
Share saved reads per project and merge them, not replace them
One record per hub meant two widgets of one kind (or one hub in two
projects) overwrote each other's saved reads, last writer winning. The
record is now per organisation, ADO user and project, shared by every
hub and widget of the project, and each save merges into it inside one
IndexedDB transaction: the newer read of a key wins, reads older than a
day go, and a record from another extension version is replaced.
Because saved reads now outlive the in-memory cache, cacheTime goes
back to ten minutes (it was a day, which held file contents and logs in
memory for that long); useGh takes a cacheTime for large reads.
dropQueries also forgets the saved copies under its key, so the next
visit after a write doesn't start from them.
invalidatePrPage drops only the PR's page and edit reads: dropping the
whole PR refetched every part of it after each action (18 requests in
the mock), and the rest already follow the action store's version.
Read the pull request page, New pull request and hover cards through queries
Switching a PR's tabs, going back to the list and reopening a PR all
started from a spinner. The PR page's reads now go through useGh under
["pr", repo, number, …], so tabs and revisits show what they had while
refreshing; the header is kept between visits.
- The header still re-reads on an action or feed version bump; edit
data, deployments and work items re-read on the action store's
version (useRefetchOn) and keep what they show meanwhile. actionStore
and feedStore are unchanged. - Files, updates, ranges, viewed states and annotations are cached for
the visit but never saved (they hold patches). A viewed tick writes
into the cached answer and is put back if GitHub refuses. - New pull request reads under ["prs", "new", …]; the preview and the
form's file count share one compare read. - Hover cards read the client directly (fetchQuery), since they also
open in Monaco zones and widgets with no provider; an answer is fresh
for ten minutes. The Monaco zone roots in PrFiles get a provider. - My PRs (the page and the My work tab) is kept between visits; being
signed out is an error, so it is never saved. The My work tab's own
entry renders inside QueryProvider.
Diff file contents, blobs and rendered markdown stay as they were.
No hand-rolled markup.
Read Actions, Advanced Security and the widgets through queries
The runs list, run pages, environments, library and alert lists
re-fetched from scratch on every revisit and filter change. Their reads
now go through useGh under the "actions" and "security" roots.
- Polling is refetchInterval: the runs list every 10 s while any loaded
run is active, a run page every 5 s while it or a job is; both stop
when nothing is running and while the frame is hidden (2 reads in 21 s
visible, none in 22 s hidden in the mock). - Writes (re-run, approve, delete logs, Run workflow, enable/disable,
library and alert changes) drop the reads they affect; deleting a run
removes it from every cached runs list at once. - Runs and alert lists are infinite queries: filters keep the rows
until the new ones arrive, and a failed "Load more" keeps the rows and
says so in a MessageCard. - Kept between visits: runs, workflows, environments, deployments,
alert lists and counts, widget data. Never logs, artifacts, test
results, run detail or permissions. Job logs leave memory after five
minutes. - The widgets render inside QueryProvider; being signed out is an error
there, so it is never saved.
No hand-rolled markup: the new warning is a MessageCard.
Show sign-in and permission errors over saved data; save one page per list
Three things found reviewing the move to the query cache, before release:
- The pages show an error only when there is nothing else to show, so a
sign-in that lapsed overnight left yesterday's saved rows on screen
with no way to sign in. A read GitHub refuses for sign-in ("auth") or
permission ("access") now clears what it showed, so every page falls
through to its error view (with Sign in), and forgets the saved copy:
every saved read for "auth", that read's for "access". Rate-limit and
network failures still keep what is shown. Done once, in the query
cache's onError, so it covers useGh and the direct useQuery pages. - On a revisit TanStack re-reads every page an infinite list holds, one
after another: five "Load more" pages saved meant five requests (each
with GraphQL extras) every time the h...
v0.43.0
What's changed
Open a Files row with a click anywhere on it
In the Files listing a folder or file only opened from its name, so a
click on the rest of the row did nothing. Native Repos opens the row from
anywhere on it. The listing's Table now uses singleClickActivation and
opens the entry (submodules still don't open). The Table leaves clicks on
links and buttons alone, so the name link, the commit link and the row's
"..." menu behave as before (checked in the mock harness: the menu opens
and the address stays the same).
Let Blame use the card's full width
Blame showed the code in a narrow strip that scrolled sideways, with
empty space to its right. The card's content is a flex row and the blame
block inside it wasn't allowed to grow, so it kept the width of its first
render, and Monaco (which follows its container) stayed that narrow. The
block now grows to fill the card: measured in the mock harness at 1237px
of 1237px, against 932px before.
Release 0.42.1
A click anywhere on a Files row opens it, as in native Repos, and Blame
uses the card's full width instead of scrolling the code sideways in a
narrow strip.
Draft and edit releases in a side panel
"Draft a new release" and "Edit release" replaced the whole Releases
tab with a form, which isn't how native ADO handles create and edit: it
opens them over the page. The form is now an azure-devops-ui Panel
(large) titled with the action and the repository, over the releases
list, or over the release itself when editing. Cancel, Save draft and
the primary button (Publish release / Update release) sit in the
panel's footer and show "Saving…" while they work. Closing the panel is
Cancel, so a changed form still asks before discarding, and the panel
can't be dismissed while a save is running.
The address still carries releaseForm (and release when editing), so a
link or a refresh opens the panel again. The detail view now stays
behind the panel when editing; with releaseForm=new the release key is
only the tag to preset (Tags' "Create release"), so the list stays
behind it. Nothing hand-rolled.
Release 0.42.2
Drafting and editing a release happen in a side panel over the page,
as native ADO's create and edit forms do. Also in this release (0.42.1
was never published): a click anywhere on a Files row opens it, and
Blame uses the card's full width.
Go back from a release with the Header's back button
A release's own page had an "All releases" button above the card.
Native detail pages go back with the Header's back button, as our PR,
Branch policies and alert pages already do, so the Tags & Releases
Header now has one ("Back to releases") while a release is open, and
the button is gone. The release card keeps its own title, so the Header
keeps the hub's.
Lay out New pull request as native does
The New pull request page put the title and description on the left,
GitHub-style side panels for reviewers, work items, tags and the rest on
the right, the Files and Commits preview underneath and the Create
button under the description. Native ADO has the branches under the
title, then Overview / Files / Commits tabs (with counts), and Overview
is a single card: Title, Description (with "Add commit messages" beside
its label), Reviewers, Work items to link (with a count and "Clear
all"), Tags, then "Set auto-complete" and the Create split button at the
card's foot, right-aligned.
The page now follows that. The card is 960px at most, as native's. The
reviewer, work item and tag pickers are always-open fields in the card
instead of panels with an Add button; picking adds the item beneath and
the field empties for the next one. GitHub's Assignees, Milestone and
Projects, which native has no counterpart for, follow native's fields
in the same style. "Add commit messages" appends the commits' subjects
to the description as a list. The Files count is loaded with the
comparison (the Files tab reuses the cached answer). The header's back
button is gone, as native has none.
AddSearch, PeopleAdd and the pickers built on them take an inline
flag for this: no Cancel button and no focus grab; the PR page's panels
keep the old behaviour. Nothing hand-rolled; the label actions are
Buttons positioned beside FormItem labels.
Release 0.43.0
New pull request is laid out as native's: Overview / Files / Commits
tabs and one card with the form and Create at its foot. Drafting and
editing a release happen in a side panel, a release page goes back with
the Header's back button, a click anywhere on a Files row opens it, and
Blame uses the card's full width. (0.42.1 and 0.42.2 were never
published.)
Bring New pull request's fields closer to native
Side by side with native's New pull request, ours still differed in how
the fields behaved:
- Reviewers and Assignees are now azure-devops-ui's multi IdentityPicker:
the chosen people sit inside the field as persona chips with an x and a
+, as native's Reviewers does, instead of a search box with a list
underneath. Tags and Projects are TagPickers with their pills inside
the field (tags keep GitHub's label colours). - Description follows native's order: the text, a character counter
(GitHub's 65,536 limit; over it, Create is blocked with a reason),
"Markdown supported." and how to link work items, the formatting
toolbar below the text, then a live preview rendered by GitHub. The
preview waits for typing to pause (600ms). CommentBox gains
toolbarBelow and footer for this; the release form's preview moves to
MarkdownPreview.tsx and is shared. - Linked work items read as native's: type icon, "Type ID: title"
linking to the item, then "Updated , " with the state's
own colour, and x. The work item lookup now also asks for
System.ChangedDate, and the type list's state colours are kept.
The PR page's own reviewer and tag panels are unchanged. Nothing
hand-rolled.
Full changelog: v0.42.0...v0.43.0
v0.42.0
What's changed
Add an organisation-level GitHub pull requests page
The My work flyout's GitHub tab shows your pull requests from every
project, but its "View more" could only send you to github.com, and
every other page we have belongs to one project. ADO's organisation home
has a hub group of its own, ms.vss-tfs-web.suite-me-page-hub-group
("Home"), and Microsoft's Search extension already adds its
organisation-wide page to it, so an extension can contribute a hub that
belongs to no project. This adds one: "GitHub pull requests", at
dev.azure.com/{org}/_apps/hub/TrueNorthIT.github-repos.github-my-prs-hub.
The home page's own My pull requests tab is a built-in component, not a
contribution point, so a tab beside it isn't possible.
The page is laid out like the Pull requests hub's Mine tab: collapsible
"Created by me" and "Review requested" cards with a count, each row
showing the repository, the project that connects it ("and 1 more" when
several do) and when it was last updated. Clicking a row opens the PR in
its project, or a menu of projects as the flyout does. Refresh and View
on GitHub sit in the header. The flyout's "View more" now opens this
page.
The data and the open-in-project handling move from the flyout tab into
myPrsShared.tsx, so both use the same search, project index and menu.
The page loads hub.html like the other hubs; index.tsx renders it when
the contribution id is github-my-prs-hub. Nothing hand-rolled. Mock
harness: ?hub=myprs.
Full changelog: v0.41.0...v0.42.0
v0.41.0
What's changed
Open My work's GitHub pull requests in their ADO project, and scroll
The first test of the GitHub tab in the My work flyout showed the tab
does render, but its frame grew to fit every row, so the list ran off
the bottom of the flyout with no way to scroll and no footer. The frame
no longer resizes itself to its content: it fills the tab, the cards
scroll inside it and "View more" stays pinned at the bottom with a rule
above it, as on ADO's own Pull requests tab. If the flyout ever hands
the frame a small default height instead, it asks for a flyout-sized
one.
Rows used to open the pull request on github.com. They now open it in
our Pull requests hub, in the ADO project that connects the repository
(an Azure Boards GitHub connection), and the tab lists only pull
requests in repositories some project connects. When several projects
connect the same repository, a click opens a menu of those projects
(an azure-devops-ui ContextualMenu); the title's href is the first
project's page, so middle-click and "open in new tab" still work.
Finding those projects means listing the organisation's projects, which
needs the vso.project scope, so this release adds it. An organisation
administrator has to approve the new permission before this version
installs. Each project's connections are read six at a time and the
result is cached in the browser per organisation for 30 minutes, so the
flyout normally renders from the cache and refreshes in the background.
Without the scope the tab falls back to the project the page is in.
Search now asks for 100 results, because filtering happens afterwards.
Mock harness: a second project, "Platform", that also connects
customer-portal, a PR from a repository no project connects (which
must not show), and noProjectScope and noConnections scenarios.
Full changelog: v0.40.0...v0.41.0
v0.40.0
What's changed
Add a test GitHub tab to the My work flyout
Azure DevOps has no documented place for an extension in the top bar, so
there was no global way to see the GitHub pull requests you created or
were asked to review. The org's contribution tree shows that the My work
flyout (the checklist icon) is an ordinary ms.vss-web.tab-group,
ms.vss-tfs-web.my-work-pivot, with ADO's own Work items (900), Pull
requests (1000) and Favorites (1100) as ms.vss-web.tab children and no
restriction on who may contribute. This adds a "GitHub" tab at order
1050, mainly to find out whether the flyout renders an extension's
iframe tab at all (its own tabs are built-in components, not iframes)
and what context it hands it; the first load logs that to the console
as "[GitHub my work]".
The tab lists open pull requests across every repository the GitHub App
can see, in two cards like ADO's own: "Created by me" (author:@me) and
"Review requested" (review-requested:@me). It uses REST /search/issues
rather than GraphQL so it can be checked in cloud sessions too. Rows link
to github.com for now; opening them in our PR hub needs a map from
repository to the ADO project that connects it, which comes once the tab
is known to work. Signed-out users get a sign-in button.
Built from azure-devops-ui throughout (Card, Header, Table with
TwoLineTableCell, VssPersona, Pill, MessageCard, Button); nothing
hand-rolled. Mock harness: myWorkTab.html, with signedOut and rateLimit
scenarios.
Full changelog: v0.39.3...v0.40.0
v0.39.3
What's changed
Update the PR timeline straight after a reviewer, label or state change
Seen live in ADO: adding a reviewer or requesting a Copilot review worked,
but the Overview timeline only showed the event after a page reload. Every
successful action dropped the cached feed (invalidatePrPage), but the feed
is its own store and nothing told it to reload, so it kept what it had.
Successful actions now reload the feed at once, and again with the existing
3-second settle refresh (clearing the cache first), because GitHub can add
the timeline event a moment after the mutation returns.
Space the Actions run page's cards and inner tab bars as native Pipelines
Reviewed live in 0.39.2: the Summary card sat flush under the page tab
bar, and the annotations card looked cramped between its Errors/Warnings
tabs and the first row.
Measured with getComputedStyle on native build results pages in
dnceng-public (1400px viewport):
- page tab bar 48px; first card 16px below it; cards 16px apart
- the issues and Stages/Jobs cards are bolt-table-card (no card padding),
16px of top padding, then a 40px large-link tab bar indented 12px - issues: 12px below the tab bar to the first 59px two-line row, no rule
- Stages/Jobs: 16px below the tab bar, then a 1px rule over the graph
Ours before: tab bar to Summary card 0px; annotations card 20px card
padding, a 40px bar holding 48px "tall" tabs that overflowed it by 4px
each side, a 1px rule and rows straight after; Stages/Jobs the same bar.
Ours after: 16px, 16px between cards, and both cards match the native
numbers above (16 + 40 + 12 to the first row; 16 + 40 + 16 + rule).
Both cards now use Card with bolt-table-card and TabBar
tabSize={TabSize.LargeLink}, as native does, in place of CSS height
overrides. The Tests tab's results card shares the annotations card's
class, so it gets the same treatment for consistency (native's Tests tab
has no tabbed card to copy). Nothing hand-rolled.
Give the alert page the Header's own back button, back to the filtered list
The alert detail page had a "Back to dependencies alerts" Link above its
title. Native detail pages use the Header's back button instead, as our
PR page and Branch policies page already do, so the alert title Header
now has backButtonProps with the same label as its tooltip and aria
label, and the link is gone.
The link went back to the bare tab and lost the list's filters. Opening
an alert now keeps the list's filters in the hash (state, severity,
branch, tool, rule, text, ecosystem, package, validity), and the back
button returns to that same hash without the alert, so the list comes
back exactly as it was left. The row's own href is unchanged.
Lay out the alert Details card across its full width, with ids that never break
Reviewed live in 0.39.2: the Details card's label/value list sat in a
narrow column (the card body is a flex row and the list shrank to its
content), so values broke mid-token: "CVE-2026-10 3923",
"package-\nlock.json", ">= 0.11.0, <\n0.18.2".
The card now holds a grid of label-over-value pairs as wide as the card
(auto-fill columns of at least 220px), labels in secondary text. Values
wrap only at spaces; ids, versions and ranges stay on one line, and they
and the manifest path end in an ellipsis with the whole value as the
tooltip if a column is too narrow. A dismissal comment spans the grid,
last. It is the same Details card for Dependabot, code scanning and
secret alerts.
CWE and CVE ids, and a Dependabot alert's GHSA id (new "Advisory" field),
are Links to their advisory pages (NVD, MITRE, GitHub advisories), built
by identifierUrl in lib/securityModel.ts, with tests.
Hand-rolled: the grid is styled divs. azure-devops-ui has no property or
description list component; Link and Pill are the library's.
Shots: security-alert-dependabot, -code and -secret.
Collapse the PR's checks to one row per workflow, with a Checks tab for the rest
Reviewed live in 0.39.2: with many workflows the Overview's policies card
was very tall. Every workflow group (Publish, Dependabot auto-merge,
Storybook Pages, UI screenshots...) was open with every job row, each row
54px, and nothing separated one group from the next.
Why every group was open: a group first seen failing or running was
marked open and then kept that as if the user had chosen it, so it stayed
open after it passed. reconcileOpen (lib/checkGroups.ts) now keeps only
the user's own toggles; an untouched group follows its state, open while
it failed or is running and collapsed otherwise. A toggle still wins,
and a collapsed group that goes from passing to failing still opens. It
takes the rule for untouched groups as a parameter, so the Checks tab can
open everything. Tests cover the running-then-passed case that bit us.
Overview: the summary header and the policy rows as before, then one row
per workflow or app (status, name, Required pill, "3 checks · 1 failed"),
with a 1px rule above each group and "View all checks" at the foot of the
card. Rows are now 45px (were 54-55px). No active pull request in a
public Azure DevOps org was reachable to measure native policy rows, so
this matches native's single-line list rows, measured with
getComputedStyle on Pipelines' Jobs list in dnceng-public (45px), which is
also our Branch policies list's row height (45px). The Re-run buttons now
sit before Details, so Details lines up down the right-hand side.
Checks tab (Overview / Files / Updates / Commits / Checks, hash
prPage=checks): every workflow and job, all open to start, with status,
duration, annotation count, Details and the Re-run buttons. It is the
same Tree, rows and re-run code as the Overview in an "all" mode, not a
copy. Durations come from the check runs' startedAt/completedAt
(checkDurationMs in lib/policies.ts, with tests) and are left off skipped
jobs. A pull request with no checks gets a ZeroData; a failed load an
error MessageCard with Try again.
Mock PR #312 gets job timings and two skipped workflows (Publish with
three jobs, Dependabot auto-merge), so it has seven groups: two failing,
one running. Shots: pr-312 and pr-312-checks.
Components: Tree with ExpandableTreeCell, Status, Pill, Link,
SplitButton, Card, ZeroData, MessageCard, TabBar/Tab. Nothing new is
hand-rolled; the separators are CSS on the Tree's rows.
Keep the alert's severity pill its own width in the Details grid
The new Details grid put the Pill inside a value span; azure-devops-ui's
Pill is a block flex box, so it stretched across the whole 220px column.
It is inline-flex there now, as it was in the old list.
Release 0.39.3
The PR timeline shows reviewer, label, vote and state changes as soon as
they're made; a PR's checks collapse to one row per workflow with a new
Checks tab for the full list; the Actions run page spaces its cards as
native Pipelines does; and alert pages get the Header's back button and a
full-width Details card whose ids and versions never break mid-token.
Full changelog: v0.39.2...v0.39.3
v0.39.2
What's changed
Show the commented code above review threads in the Overview feed
A thread in the activity feed showed only a "path, line N" link, so a reader
had to open Files to see what the comment was about. Native ADO and GitHub
both show the commented lines above the thread; now we do too.
The lines come from the first comment's diffHunk, which the feed already
loads: the last four lines up to the commented line, or exactly start..end
for a range comment, with old and new line numbers and the +/- tint our
suggestion diff uses. lib/hunkContext.ts parses the hunk (several @@
headers, LEFT-side comments, ranges starting on the other side, "no newline"
markers, CRLF). The hunk is numbered as the code was when the comment was
written, so it is located by originalLine/originalStartLine, which the
thread query now also selects (with startDiffSide). An outdated thread
therefore still shows the code it was written on, beside its Outdated pill.
Hand-rolled: the snippet is a small table reusing the Advanced Security
snippet's styles. No azure-devops-ui component renders a few diff lines, and
a read-only Monaco per thread is too heavy for a feed. Files threads are
unchanged: the diff is already beside them.
Mock: the outdated thread on #200 now carries the hunk it was written on.
Shots page pr-200-threads.
Show Copilot findings' title and severity in review threads
A Copilot review comment's body is only the finding's paragraph. Its title
("Expand and JSON-quote the default reasoning effort") and severity live in
the parent review's body, an overview () listing each
finding with a severity badge and a link to #discussion_r. The
feed already loads review bodies, so no request is added: toFeed reads the
overview of every review by copilot-pull-request-reviewer (only that bot's;
later reviews win) and gives each of its comments the matching title and
severity, keyed by the comment's databaseId.
The thread then shows the severity as the Advanced Security hub's pill
(High red, Medium amber, Low blue; an unknown word as an outlined Pill) and
the title in bold above the body, in the Overview feed and in Files.
copilotFindings (lib/copilotReview.ts) is tested against a trimmed copy of a
real Copilot review body. The review's own overview renders acceptably as it
is: the sanitiser drops (alttext if it cannot load), and
Mock: #200 gets a Copilot review in the current overview shape, with its
GitHub-rendered HTML, and two Copilot threads on src/retry.ts. Shots page
pr-200-copilot.
Link Copilot's suggested fix to GitHub from its review comments
GitHub shows a "Suggested changeset" under Copilot's review comments, but no
public API field carries it (not in the REST comment, the GraphQL comment or
the review), so it cannot be shown or applied here. Rather than let it
vanish, each Copilot comment now ends with a quiet line, "Copilot suggested a
fix.", and an "Open on GitHub" link to the comment, in a new tab.
Ordinary ```suggestion blocks are unchanged: they still render with Apply.
Show Copilot and other bots as themselves, with their own hover card
Comment headers showed Copilot's raw login, copilot-pull-request-reviewer,
and hovering Copilot in the reviewers list opened a person card for the bot
account: a big generic avatar, its literal bio ("test") and a link to the
bot's profile.
lib/botIdentity.ts decides, from what we have (the login in its GraphQL,
REST and review-comment spellings, GraphQL __typename or REST user.type,
REST is_copilot_code_review), whether an author is Copilot (name "Copilot",
an "AI" pill, as GitHub labels it), another app (its name without "[bot]",
a "Bot" pill, its https://github.com/apps/{slug} page) or a person.
- ActorName renders the name and pill: feed and Files thread comment
headers, review cards and events, reviewers, the PR author on the list,
page and widget, and edit history. - PersonHover gives a bot a card built from that, with no /users lookup:
Copilot's says "AI code review by GitHub Copilot" and links "About
Copilot code review" to GitHub's docs; another app's links to its app
page. A card rather than none, so every avatar behaves the same. - Comment and review authors in the feed now have hover cards at all.
- The feed and PR queries select the author's __typename, so an app whose
GraphQL login has no "[bot]" suffix is still recognised. - reviewerName now goes through the same helper.
Mock: Copilot commented on #200, so it is in the reviewers list. Shots page
hover-copilot. Note: on this machine headless Chrome draws no hover card on
any hover page (hover-person and hover-reviewer included; the card is in the
DOM but placed off-screen), so the cards were checked in the DOM.
Give every person a hover card, including commit authors with no GitHub account
Hover cards were keyed on a GitHub login, so a commit whose email is on no
GitHub account (all of tn-local-agents' commits: "Bogdan <BMichon@…>",
author.user null) got no card, on the Files Commits column and the Commits
rows alike; and some places that show a person had no card at all.
lib/identityCard.ts chooses the card from whatever we have:
- a bot (botIdentity.ts): the Copilot or app card;
- a GitHub login: the existing profile card (GET /users/{login});
- no login but a git name and email: look the email up in Azure DevOps'
own directory, through the host's identity service (the one its identity
picker uses), and show azure-devops-ui's native IdentityCard when exactly
that email is someone in the organisation (pickAdoIdentity refuses the
name matches the search also returns). Otherwise a card in our Callout
frame: avatar, git name, the email as a mailto link, and "This commit's
email isn't linked to a GitHub account." Never /users/{login}. - the ghost, or nothing to go on: no card.
The native IdentityCard draws its own light-dismiss callout, so HoverCard
gained an unframed mode that hands the anchor and the open/close timing to
the card through context; pointer moves over the card or the anchor keep it
open and moving away closes it after the usual grace period.
Plumbing: the git email is now kept where it was dropped: the commit,
blame, branch, commit detail (author and committer), Files history, PR
commits and feed push queries select author.email; REST commits keep
commit.author.email; Person, CommitInfo and PrCommit carry it with the login.
New hover-card anchors: Files' Commits column, the PR Commits tab, feed
event actors (pushes as the git identity), forks' owners, the Advanced
Security "dismissed by", and the new PR's reviewer and assignee rows.
PersonLine (Commits rows, commit detail, branches, tags, releases, blame,
Actions, environments) now passes the email, so git-only authors there get
the card too. Left out on purpose: the viewer's own avatar beside the
comment boxes, the @mention and reviewer-picker suggestion lists (a card
would fight the dropdown), and the author line inside a PR hover card
(no cards inside cards).
Not verified live: whether the host's identity service answers this
extension's frame with its current scopes (vso.githubconnections, vso.work);
no scope was added. A failed lookup falls back to the plain card.
Mock: git-only authors on the Commits page (Gwen Git, in a mock ADO
directory in mockSdk.ts; Sam Unlinked, in none) and Carol on Files.
Shots pages hover-git-ado and hover-git.
Fix Files' blank commit columns and branch label after a branch switch
Found live in ADO on tn-local-agents. Switching from main to a branch with
more files left most rows without Last change or Commits: the new ref
rendered one frame before its folder listing, so the commit lookup ran with
the old branch's two paths under the new ref, and getCommits cached that
answer by ref and folder alone. The key now carries the entries' paths.
The branch picker also went blank after a switch: the Dropdown keeps its
own selection, and once the branch list reloads the picked item is gone,
hiding the placeholder that carries the branch name. It now remounts per
ref.
Release 0.39.2
PR Overview threads show the code they comment on; Copilot review comments
show their title, severity and a link to their suggested fix, and Copilot
and other bots appear as themselves; every person's name or avatar has a
hover card, with Azure DevOps' own card for commit authors whose email
isn't linked to GitHub; and Files no longer loses its commit columns or
branch label after switching branch.
Full changelog: v0.39.1...v0.39.2
v0.39.1
What's changed
Open GitHub's code search instead of searching in the extension
The Code search page called GitHub REST GET /search/code from the browser.
GitHub answers that endpoint's preflight with CORS headers, but its real
responses carry no Access-Control-Allow-Origin (unlike /repos/... and
/search/commits), so the browser drops them and the search can never work
from the Azure DevOps iframe. A relay would fix it only by putting a server
in the data path, and the extension is going public with none, so that is
ruled out.
The "Search code" box at the end of the Files path bar now opens GitHub's
code search for the current repository in a new tab. Its placeholder reads
"Search code on GitHub" and it carries an OpenInNewTab suffix icon
(TextField's suffixIconProps) so it is clear it leaves the extension.
Removed: the Code search page (CodeSearch.tsx) and its route in App.tsx,
the search hash keys (lib/searchHash.ts and the search entry in
useHashExtras), searchCode and RawSearch in githubBrowse.ts, the search
preview viewer in monacoChunk.ts, the /search/code mock and its
searchRateLimit, searchSecondary and searchInvalid scenarios, the search
shots (mock and live), and codeSearch.css (the box's one width rule moves
to browser.css). lib/codeSearch.ts keeps only githubSearchUrl and
stripScope, with their tests. CLAUDE.md and docs/live-check-gaps.md now
say why code search links out.
Widen the Clone URL box and shorten disabled branch-menu reasons
Found in the live check of 0.39.0 in Azure DevOps. The Clone panel's URL
box stayed at its natural width (about 150px, cutting the URL off): a
TextField's className lands on the inner field, so flex-grow never reached
the row; containerClassName is the prop that sizes it. And the disabled
"Delete branch" item's reason was a whole sentence in secondaryText, which
ran past the menu's edge; menu items have no tooltip, so the reasons are
now the short labels native uses there ("Default branch", "Protected").
Release 0.39.1
Fixes from the first live check of 0.39.0 in Azure DevOps: code search now
opens GitHub's own search (its API sends no CORS header, so the extension
can't read the results), the Clone URL box fills the panel, and disabled
branch-menu reasons no longer run past the menu.
Full changelog: v0.39.0...v0.39.1
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 Me...