In short: the webview page holds two custom elements, a hidden jj-context-menu-provider
and jj-app. Each time the extension sends a state, jj-app lays out the
commits (see graph layout) and renders a search box plus one
jj-commit-graph per repository. A graph is a repository name, a top bar of buttons,
callouts, one jj-commit-row per commit and a line about hidden commits. A row is a
display ID, the tiles of the graph on its left, and on its right the chips, title and action buttons.
Every user action becomes a call on the extension API (see graph protocol).
The webview never runs jj itself.
1. The page and the app shell
getHtmlForWebview writes a small page. It
loads codicon.css, _jj_graph_base_styles.css and the module
webview_module.bundle.js, and its body holds just
<jj-context-menu-provider> and <jj-app id="jj-app">. The page sets
no Content-Security-Policy meta tag of its own. The bundle's entry point, which connects
jj-app to the extension, is covered under graph protocol.
jj-app extends the
resize controller and imports the
@vscode-elements/elements widgets used across the tree.
-
setStateruns once per message. For each repository state it keeps only the focused commits when focus mode is on (the options carry adisableFocusModecommand exactly then, per its documentation), lays them out withcreateCommitNodes, sorts the nodes top to bottom and tells the extension the resulting order with$informNewOrder(children of each parent, top-to-bottom hashes, the working-copy index). It setsmultiSelectionModewhen more than one node is multi-selected andunfocusedCommitsto the number hidden. It resolves on the next animation frame, so the caller knows the DOM has caught up. -
renderdraws the search box (only when the webview options enable search) and onejj-commit-graphper state, sized to the controller'srenderedWidth. It swallowscontextmenuevents, because otherwise VS Code shows its default Cut/Copy/Paste menu, which does nothing here. -
calculateRenderModeturns theAUTOrender mode into one line per commit when the view is wider than 400 px and two lines otherwise. A click anywhere on the app clears the multi-selection (source).
2. Graphs and rows
jj-commit-graph renders the repository name, a
top bar (only if the state has topBarButtons), the callouts, the rows and
jj-focus-mode-text. The rows sit in a relatively positioned wrapper that clips
horizontally, so a very wide graph does not add a horizontal scroll bar, but lets content overflow
vertically, so the rebase hint above the top row is no longer cut off by the top bar
(source).
jj-commit-row is one commit. In one-line mode it renders the
display ID, the left side and the right side on a single 24 px line. In two-line mode it renders two
lines: the first has the left side, the display ID and the chips, the second the left side again and
the title (source). Selection shows as attributes: the working
copy (active) uses the inactive-selection background, multi-selected rows the
active-selection colours, and a drop destination --vscode-list-dropBackground.
-
A click calls
$onClick(repo, hash, metaOrCtrl, shift), unless the event was alreadydefaultPreventedby a chip link (see chips). A double click without Cmd/Ctrl runs the node'sdblClickcommand, if it has one (source). - The left side is drawn by tiles, glyphs and lines, which belong to the layout and are described in graph layout: the rendering components and where a line actually appears.
-
jj-commit-row-right-sideholds the title and the action buttons: one icon pernode.buttonsentry and, ifshowContextMenuIconis set, a "More" kebab that opens the row's context menu. The buttons hide until the row is hovered unlessalwaysShowActionsis set. The whole right side is wrapped in a drag-and-drop publisher for the commit. -
jj-commit-row-titleshows the chip group, then "conflict" and "divergent" in the error colour, then metadata text, then the short description. The metadata followscommitMetadataTextStyle: either "(empty)" and "(no description set)", or "(empty)" and "(has changes)" without the redundant combination (source). Immutable commits show ajj-time-ago-textinstead of a description:timeAgoTitle(or "Submitted") followed by the commit's age, refreshed every minute by a timer.
The display ID
jj-commit-row-display-id renders the change ID in a
monospace column of fixed width: maxChars × 7.5 × uiScaling + 4 px, the longest ID in the
graph, or 0 when showChangeId is off (source).
The first highlightedDisplayIdLen characters are bold in
--commit-row-highlighted-text, the rest plain, and anything after a / bold
again; divergent IDs use the error colour
(source). Because the ID is split across several text nodes,
the element supplies its own search matches (source).
Chips
jj-commit-row-chip-group renders one draggable split chip
per node.chips entry. While a chip from another row is dragged over this row, it also
shows a preview copy of that chip (source). The chip being
dragged fades to opacity 0.3 in its own row
(source).
-
A split chip has a left half and an optional right half,
each with its own colours, codicons and command or link. Right-clicking a chip with a
vscodeContextopens the chip's own context menu and stops the row's. The split chip tracks hover over the whole element and passes it down to both halves, so hovering either half counts (source). -
A link chip is a real
<a>. Middle click is turned into Cmd/Ctrl+click. A plain click callspreventDefaultrather thanstopPropagation: VS Code has to see the click to open the link safely, and if it can't, the webview navigates and is destroyed by its frame policy. The row then ignores the prevented click (source). -
While the search box is open a chip shows its
searchText, if it has one, so that hidden text can be found; otherwise, while hovered, it showstextOnHoverif it has one (source). A chip with no text to show and no codicons renders nothing (source).
3. Top bar, callouts and hidden commits
jj-top-bar is a sticky 35 px bar that gains a
shadow once the page scrolls. Clicking its background clears the multi-selection. It renders one
vscode-button per topBarButtons entry; an entry with a right half becomes a
split button whose chevron opens a context menu with origin right.origin and the
primary command (source). While anything is being dragged
the buttons are replaced by the garbage section.
jj-garbage-section is a dashed box with a
trash icon and a hint, "Drop here to abandon" or, while hovered, "Abandon", each with the number of
commits when more than one is dragged. Its colours come from the
--jj-garbageSection-* variables (styles).
jj-callout shows a message
styled as error, warning or info with VS Code's input-validation colours. A callout with a
dismissId offers "Don't show this again" or "Dismiss", which call
$dismissCallout. The message is the only HTML the webview renders from a string (see
untrusted text).
jj-focus-mode-text
renders only while focus mode hides commits: a "Disable focus mode" link that runs the state's
disableFocusMode command, then "to see the other N commits in this workspace."
4. Search
Search is entirely in the webview: it highlights matches in the rendered DOM and never asks the extension.
| Piece | What it does |
|---|---|
jj-search-box |
A fixed box at the top right with a "Find..." field, a result count ("No results" or
"i of n") and up, down and close buttons. A document key listener
(source) opens it on Cmd+F (Mac) or Ctrl+F and stops
the browser's own find. If it is already open the text is selected. Enter and Shift+Enter
step forwards and backwards, wrapping at the ends. Escape closes it and stops the event so
the app does not also clear the multi-selection. isVisible is a reflected
property, and while it is false the host element itself is display: none
(source).
|
SearchBoxStateManager |
A global singleton holding isOpen, with subscribe and unsubscribe that throw on
a duplicate or unknown callback. Chips subscribe so they can switch to their search text.
|
highlight |
Clears CSS.highlights for an empty query. Otherwise walks the tree under
#jj-app and registers every match as the CSS custom highlight
search-matches.
|
findMatchesRecursively |
The walk. An element that implements getMatches answers for itself (the display
ID does; the search box answers with none). Otherwise it descends into shadow roots, into the
nodes assigned to a <slot>, and into children, skipping comments.
|
calculateRanges |
Case-insensitive, non-overlapping matches over a run of text nodes, so a match can span more than one node. |
scrollRangeIntoView |
Marks the current match as search-current-match and scrolls it into view
instantly, leaving room for the sticky top bar (35 + 8 px) above and 16 px below.
|
5. Drag and drop
Dragging is how the graph rebases, moves bookmarks, inserts and abandons. Three pieces cooperate
through one global DragAndDropStateManager.
Targets
A Target is one of four things: a
commit, a chip on a commit, an insert action (an edge of the
graph, from the layout's tiles) or the
garbageSection. isDraggable allows
a drag only when the state supplies dragAndDropCommands: never from an insert action or the
garbage section, from a chip only if that chip has its own commands, never from an immutable commit,
and in multi-selection mode only from a selected commit.
State and subscribers
-
The manager keeps the dragged target and a stack of hovered targets with enter and leave counts,
because nested elements fire several
dragenter/dragleavepairs. It ignores entering the dragged target itself, and entering one multi-selected commit while dragging another.dragoveralways callspreventDefaultso drops are allowed. On macOSdragstartsetseffectAllowed = 'move'to hide the green plus cursor; this is skipped on Linux, where it flickers (source). -
isNoopInsertsuppresses hints for drops that would change nothing: a chip over its own commit, an insert before an immutable commit, or an insert whose either end is the dragged commit. -
JjDragAndDropSubscribergives an elementisDragSourceandisDragDestinationfor its own target (a hovered chip counts as its commit).JjDragAndDropAllTargetsSubscriberexposes the rawdraggedandhoveredtargets instead. -
The publisher sets the drag image: a 0.7-opacity clone of a dragged chip, or a blank image for a
commit
(source). It reports
dragleave10 ms late because Linux delivers enter and leave out of order.
What a drop does
| Dragged | Dropped on | Command call |
|---|---|---|
| chip | another commit (or a chip on it) | the chip's move command with the destination hash |
| chip | garbage section | the chip's abandon command |
| commit | commit or chip | rebase: the dragged commits (all selected ones in multi-selection mode) onto the destination |
| commit | insert action | insert with {target, insertBefore, insertAfter} |
| commit | garbage section | abandon, fired without waiting |
The mapping is handleDragEnd; the commands
themselves come from the chip's or the state's dragAndDropCommands, so the webview
only knows the shape of the call. While a drag is on, svgRebaseHint
draws a curved arrow and an 85×20 "Rebase here" bubble, and
svgInsertHint with
svgInsertNode shows a ghost
node and "Insert between", "Insert as common parent" or "Insert as common child". These used to be
custom elements with their own SVGs; they are now plain functions returning SVG groups that the
glyph tile paints into its one SVG, part of an upstream change to cut the number of SVGs in the
graph (see graph layout: the rendering components). The
bubbles share HINT_BUBBLE_STYLES,
now scoped to a .hint-bubble class: no pointer events, a drop shadow, and the button
colours. The old 10 px cap on the hint's box, which stopped a hint on the bottom row from growing the
graph, went away with the separate SVGs; the hint now lives inside a tile-sized SVG with visible
overflow.
6. Context menus
A webview cannot open a VS Code menu directly. Instead VS Code opens one on a
contextmenu event and passes the menu items the element's
data-vscode-context JSON. openContextMenu
uses that: it writes the JSON onto the hidden #jj-context-menu-provider button and
dispatches a synthetic bubbling contextmenu at the mouse position. The provider renders
into the light DOM (source) so VS Code can find the button.
-
A row first awaits
$informContextMenuWillOpen; an upstream comment says the menu sometimes fails to appear without it. The context then has origincommitRowMultiSelectedfor a selected row in multi-selection mode, or the node's ownvscodeContextwith origincommitRow(source). The kebab button calls the same method. -
Chips and top-bar split buttons build their own contexts (see chips and
top bar). Every context sets
preventDefaultContextMenuItems.
7. The resize controller
JjResizeController is the base class of
jj-app. VS Code delivers webview resize events out of order while a panel is dragged,
so rendering every event makes the graph jitter. On each resize
(source) the controller skips widths it has already seen,
tracks whether the width is growing or shrinking, and only updates renderedWidth when
the new width moves in that direction. A 100 ms timer always renders the latest width in the end and
resets the history. Widths come from the host's computed style;
parseWidthString throws on anything but pixels.
8. Untrusted text
Commit descriptions, bookmark names and other repository text reach the page as strings in the
state. Every component inserts them with Lit bindings, which set text content and escape it, so they
are never parsed as HTML. The one exception is the callout message:
safeHTML passes it through
sanitizeHtml from safevalues before Lit's unsafeHTML, and
callout.ts is its only caller.
Two contexts are built as JSON strings: the top bar's split-button context interpolates
right.origin and the command ID into a template string without escaping
(source), whereas rows and chips use
JSON.stringify. Both values are set by the extension, not by repository content.
9. Styles and theme colours
Each component carries its own Lit static styles, and colours come from VS Code's theme
variables (--vscode-*) wherever one fits, so the graph follows the user's theme. The page
stylesheet _jj_graph_base_styles.css adds the rest:
| Variable | Light | Dark | Used for |
|---|---|---|---|
--commit-row-highlighted-text | #994f00 | #fcad70 | display ID prefix, "(empty)" text |
--inactiveLineColor | lightgray | #616161 | lines and glyphs off the working-copy path (foreground in other themes) |
--jj-garbageSection-* | #939393; on hover the diff editor's removed-line background and border #f59c8d | the abandon drop zone | |
--jj-activeCommitRow-* | --vscode-list-activeSelection* | multi-selected rows | |
::highlight(search-matches) | black on yellow | every search match | |
::highlight(search-current-match) | black on #ff9632 | the current match | |
Icons are VS Code codicons through jj-codicon,
which parses names like codicon-sync~spin with
parseCodicon. One name,
codicon-checkout, is swapped for the custom SVG
jj-edit-codicon. Sizes are in
constants.ts: 14 px tiles, 24 px
rows and a 35 px top bar.
10. Helpers and tests
-
ageformats a timestamp like Mercurial'sagetemplate: "now", "N units ago" or "from now", a plain date beyond two years, and "in the distant future" beyond thirty. -
checkExhaustivemakes switches over enums fail to compile when a case is missing.isMacpicks Cmd or Ctrl. -
Tests run with Jasmine under Bazel (
BUILD.bazel):drag_and_drop_state_test.ts,search_box_state_test.ts,search_highlighter_test.ts,codicon_test.tsandtime_test.ts. The rendering components themselves have no tests.