In short: the host and the webview each wrap postMessage in a small RPC layer adapted from
VS Code's own. Each side gets a proxy for the other side's interface, so calling
webviewApi.$setState(state) on the host becomes a JSON message, a call to the webview's
implementation, and a response matched back by ID. Before any call goes out, both sides repeat a
handshake until each knows the other is listening. The webview announces itself with
$webviewReady, and the host answers with a WebviewState. At this pin that state
is a single hard-coded commit.
1. The two interfaces
ExtensionShape: implemented by the host, called by the webview
| Method | Arguments | Called when | Host behaviour at this pin |
|---|---|---|---|
$webviewReady | none | The webview module boots (source). | Sends the fake state with $setState. |
$executeCommand | repoName, command, args? | Any button, chip, double-click, drag-and-drop drop or focus-mode link that carries a VSCodeCommand. | No-op. |
$informNewOrder | repoName, Order | After every layout, from JjApp.setState. | No-op. |
$informContextMenuWillOpen | repoName, hash? | Just before a row's context menu opens, so the host can enable or disable items. No hash means the working copy. | No-op. |
$onClick | repoName, hash, metaKey, shiftKey | A row is clicked. | No-op. |
$clearMultiSelection | repoName | The multi-selection is cleared from the top bar or the app. | No-op. |
$dismissCallout | dismissId | A dismissible callout is dismissed. | No-op. |
Order carries the layout's results back to the host:
childrenOrder (a ChildrenOrder
{parentHash, childrenHashes} per commit, in the order drawn), topToBottomOrder
(all hashes as displayed), and wcCommitIndex (the working copy's position in that list, or
-1). That lets keyboard navigation or "next/previous commit" commands in the host follow
what the user sees.
WebviewShape: implemented by the webview, called by the host
| Method | Arguments | Webview behaviour |
|---|---|---|
$setState | WebviewState | JjApp.setState: focus mode, layout, $informNewOrder, re-render. It resolves on the next animation frame, so the host's call completes once the new state has rendered. |
$openSearchBox | none | Opens the search box. Nothing in the host calls it at this pin. |
Every method name starts with $. That's how the RPC proxy recognises remote methods: a
property starting with anything else reads as undefined.
2. The transport
The RPC layer lives in third_party/vscode/rpc_protocol/, adapted
from VS Code (MIT licensed). Both sides speak to it through a two-method
Channel: postMessage(message) and
onMessage(callback) → Disposable.
| Message | Shape | Source |
|---|---|---|
| Request | {rpcProtocolId: 'jj-dojo-rpc-protocol-request', data: {name, args, id}} | rpc_protocol_utils.ts |
| Response | {rpcProtocolId: 'jj-dojo-rpc-protocol-response', data: {id, response?, err?}} | rpc_protocol_utils.ts |
| Handshake | {rpcProtocolId: 'jj-dojo-rpc-protocol-handshake', received: boolean} | rpc_protocol_utils.ts |
Messages with any other rpcProtocolId are ignored, because "We may not be the only ones
using the channel" (source). Both directions use the same code:
the host calls getWebviewApi and the webview calls
getExtensionApi. Both are thin wrappers over
getApi, which:
- creates a sender: a
Proxywhose$methods post a request with an incrementing numericidand return a promise stored in apendingmap (source); - calls the caller's callback with that proxy to get the local implementation;
- creates a receiver that answers each request by calling
impl[name](...args), awaiting it, and posting a response with the sameid(source); - starts the handshake, and returns the proxy immediately. Every proxy call first awaits a "blocker" promise that only resolves once the handshake finishes (source).
Pairing. Each side numbers its own requests independently, and a response is matched
only against the sender's own pending map, so the two directions never collide. Calls can
overlap, and responses may arrive in any order.
Errors. If the implementation throws, the receiver copies message,
name and stack into a plain object, because methods don't survive the
boundary, and sends it as err. Non-Error throws become
{name: 'UNKNOWN_ERROR'} with a stringified message. An unknown method name produces
No such function: <name>. On the calling side the code builds a fresh
Error from err but then rejects with the original plain object
(source), so callers receive an object that has
message and name but isn't an Error instance.
Disposal. Disposing the returned disposable removes the listeners and rejects every
pending call with RPC channel disposed.
3. The handshake
Webviews take time to load, and a message posted before the other side is listening is lost.
shakeHands solves this symmetrically. Each side posts
{received: weHaveReceived} every 100 ms. The first handshake message it gets sets
weHaveReceived. Once a message arrives saying the other side has received ours
(received: true), it resolves, posts one final message so the other side can finish too,
and stops. Then the blocker opens and queued calls go out.
Timeout. After 300 tries, which is 30 seconds, it rejects with
Handshake timed out after 30s. Nothing catches that rejection, since getApi
only attaches a .then, and the blocker never opens, so every later call on that side hangs
forever. The webview logs JJ Dojo commit graph started on boot, "temporarily to debug
handshake time out errors" (source).
Request IDs are per direction: the webview's $webviewReady and the host's
$setState are both id: 1 without colliding, because each side only matches
responses against its own pending map.
4. The host side
JjUi registers CommitGraphViewProvider for
the view type jj-dojo.commit-graph with retainContextWhenHidden: true, so the
webview, and its RPC session, survive the view being hidden
(source). When VS Code shows the view,
resolveWebviewView:
- sets
enableScripts: trueand limitslocalResourceRootsto the extension's own directory; - writes the HTML (
getHtmlForWebview): the codicon stylesheet,_jj_graph_base_styles.css, the bundledwebview_module.bundle.js, and a body containing<jj-context-menu-provider>and<jj-app id="jj-app">; - builds a
Channelfromwebview.postMessageandwebview.onDidReceiveMessage; - calls
getWebviewApiwith a callback that returns a newExtensionShapeImplwrapping the webview proxy; - disposes the RPC session when the view is disposed (source).
ExtensionShapeImpl is the only host implementation. A
TODO in its header says its contents "should be replaced by the Google internal code". Only
$webviewReady does anything:
it sends createFakeWebviewState(), marked
// TODO - Replace with a real implementation. That state is one graph named
jj-dojo with a single empty working-copy commit, change IDs shown, one-line rendering, and
search disabled. Every other method is an empty async function.
Multi-selection, not wired yet
MultiSelectManager sits next to the provider and holds
the set of selected commits in a HashSet. Upstream says it came from Google's internal code
with minimal changes. Nothing outside its test constructs it at this pin, so the host still ignores
$onClick.
setOrder(ids)stores the graph's top-to-bottom row order.onClick(id, metaKey, shiftKey): a plain click selects just that commit; a meta-click toggles it. Both remember it as the last clicked. A shift-click adds every commit between the last clicked and this one, inclusive and in either direction, and keeps the anchor; when there is no order or either commit isn't in it, the shift key is ignored (source).evictMissing(ids)drops selected commits that are no longer in the repository, and forgets the anchor if it went too (source).getOnlySelected()returns the selection when it is exactly one commit.
Its eight tests cover each of those paths, including matching distinct ID objects by their string (source).
5. The webview side
webview_module.ts is the webview's entry point. It imports
the JjApp component, gets VS Code's webview API with acquireVsCodeApi(), and wraps
vscode.postMessage and the window's message events in a Channel. Its
getExtensionApi callback hands the host proxy to <jj-app>, fires
$webviewReady, and returns the WebviewShape implementation, which forwards
$setState and $openSearchBox to the app. The returned disposable is deliberately
not kept: the comment says resources are released when the webview itself is disposed.
From there the components call the host through extensionApi, mostly with
$executeCommand. See the graph webview UI.
6. The state types
Everything the host sends lives in api/types.ts. All of it
must survive structured cloning through postMessage, so it is plain data: strings, numbers,
booleans, arrays and objects, with commands named by ID rather than passed as functions. The tables
start at the root, WebviewState.
WebviewState
| Field | Type | Meaning | Source |
|---|---|---|---|
states | CommitGraphState[] | One graph per repository. | L271 |
snapshotId | number (optional) | Declared but not read anywhere in the extension at this pin. | L272 |
options | WebviewOptions | Webview-wide options. | L273 |
WebviewOptions
| Field | Type | Meaning | Source |
|---|---|---|---|
enableSearch | boolean | Whether the search box is enabled. | L278 |
CommitGraphState
| Field | Type | Meaning | Source |
|---|---|---|---|
repoName | string (optional) | Repository name. If absent, no name is shown. Passed back on most ExtensionShape calls. | L289 |
callouts | Callout[] | Messages shown above the graph. | L292 |
commits | Commit[] | The commits to lay out. | L294 |
topBarButtons | TopBarButton[] | Empty: no top bar. One: a full-width button. Several: fixed-width split buttons. | L303 |
options | CommitGraphOptions | Per-graph options (above). | L305 |
multiSelectionMode | boolean (optional) | Internal, set by the webview. "The extension should ignore this field." | L311 |
unfocusedCommits | number (optional) | Internal, set by the webview: how many commits focus mode hid. | L317 |
timeAgoTitle | string (optional) | Replaces the default "Submitted" text in the base commit row. | L323 |
Commit
| Field | Type | Meaning | Source |
|---|---|---|---|
hash | string | Unique identifier. Every map in the layout is keyed by it. | L23 |
childrenHashes | readonly string[] | Children in preferred display order. Every child must be in the same commits array, or layout throws. | L33 |
shortDescription | string | First line, shown as the row title. Empty means "no description set". | L35 |
fullDescription | string | Whole message, shown as the row's tooltip. | L37 |
isImmutable | boolean (optional) | Drawn with a diamond glyph, and bounds focus mode. | L40 |
active | boolean (optional) | Marks the working-copy commit (@ glyph). | L42 |
isEmpty | boolean | Drives the (empty) tag. | L45 |
hasConflict | boolean (optional) | Drawn with a cross glyph in the error colour, plus a conflict tag. | L47 |
hasDiverged | boolean (optional) | Adds a divergent tag. | L49 |
chips | SplitChip[] | Bookmark-style chips on the row. | L51 |
iconButtons | IconButton[] (optional) | Buttons shown when the row is hovered. | L54 |
dblClick | VSCodeCommand (optional) | Command run when the row is double-clicked. | L57 |
displayId | string | The ID text to show. The comment points at "Commit.displayId in api.ts", a file not in this repository. | L60 |
highlightedDisplayIdLen | number | How many leading characters of displayId to highlight, like jj's shortest unique prefix. | L63 |
updateTime | number | Last-updated time in milliseconds since the epoch, shown as "time ago". | L66 |
isMultiSelected | boolean | Whether the row is part of a multi-selection. | L69 |
vscodeContext | {[key: string]: unknown} (optional) | Merged into the row's data-vscode-context attribute when its context menu opens. | L75 |
CommitGraphOptions
| Field | Type | Meaning | Source |
|---|---|---|---|
showChangeId | boolean | Whether to show each commit's display ID. | L132 |
dragAndDropCommands | {…} (optional) | If set, commits can be dragged and dropped. | L135 |
dragAndDropCommands.rebase | VSCodeCommand | Run on dropping onto a commit, with [sourceIds…] and the destination ID appended. | L143 |
dragAndDropCommands.abandon | VSCodeCommand | Run on dropping onto the garbage section, with every selected commit's ID appended. | L149 |
dragAndDropCommands.insert | VSCodeCommand | Run on dropping onto a line between commits, with {target, insertBefore, insertAfter} appended. | L163 |
alwaysShowActions | boolean | Show row buttons all the time instead of on hover (default false). | L170 |
uiScaling | number (optional) | UI scale factor, meant to be cssOverrides.fontSizeScale. Default 1. | L177 |
disableFocusMode | VSCodeCommand (optional) | Turns focus mode on, and is the command behind its "Disable focus mode" link (see focus mode). | L184 |
showContextMenuIcon | boolean (optional) | Show a context-menu icon on each row. | L187 |
commitMetadataTextStyle | CommitMetadataTextStyle (optional) | Which metadata tags to show (above). | L190 |
renderMode | RenderMode | One-line, two-line or automatic rows. | L192 |
RenderMode
| Member | Value | Meaning | Source |
|---|---|---|---|
ONE_LINE | ordinal | Every row is one line. | L200 |
TWO_LINE | ordinal | Every row is two lines. | L201 |
AUTO | ordinal | One or two lines "depending on the width of the panel". | L206 |
CommitMetadataTextStyle
| Member | Value | Meaning | Source |
|---|---|---|---|
SHOW_EMPTY_AND_NO_DESCRIPTION_SET | 'show empty/no description set' | Tags (empty) and (no description set) independently. Also the behaviour when the option is unset. | L123 |
SHOW_EMPTY_AND_HAS_CHANGES | 'show empty/has changes' | Tags (empty), or (has changes) for a non-empty commit without a description, and nothing when both apply. | L124 |
Callout
| Field | Type | Meaning | Source |
|---|---|---|---|
type | CalloutType | Style. | L223 |
message | string | Text, which "can contain HTML, e.g. links". | L225 |
dismissId | string (optional) | If set, the callout gets a dismiss control, and this ID is sent back with $dismissCallout. | L230 |
dismissStyle | 'button' | 'link' (optional) | button (default) or link. | L235 |
CalloutType
| Member | Value | Meaning | Source |
|---|---|---|---|
UNSTYLED | 'UNSTYLED' | No styling. | L213 |
INFO | 'INFO' | Informational. | L214 |
WARNING | 'WARNING' | Warning. | L215 |
ERROR | 'ERROR' | Error. | L216 |
TopBarButton
| Field | Type | Meaning | Source |
|---|---|---|---|
left | {…} | The main button. | L242 |
left.command | VSCodeCommand | Command to run on click. | L244 |
left.disabled | boolean | Disabled state. | L246 |
right | {…} (optional) | Optional split half. If absent, a plain button is drawn. | L249 |
right.origin | string | Opens a context menu with origin set to this value, so the extension's package.json menus can match on it. | L261 |
right.disabled | boolean | Disabled state. | L263 |
Chip
| Field | Type | Meaning | Source |
|---|---|---|---|
text | string (optional) | Label. | L331 |
textOnHover | string (optional) | Label while hovered (defaults to text). | L333 |
searchText | string (optional) | Label while the search box is open (defaults to text). | L335 |
tooltip | string (optional) | Hover text. | L336 |
link | string (optional) | URL to open on click. Ignored if command is set. | L337 |
underlineOnHover | boolean (optional) | Underline the label on hover. | L338 |
command | VSCodeCommand (optional) | Command to run on click. | L343 |
color | ChipColors<string> | Colours. | L344 |
codiconBefore | string (optional) | Codicon before the label, e.g. codicon-comment. | L346 |
codiconAfter | string (optional) | Codicon after the label. | L348 |
minWidth | number (optional) | Minimum width. | L350 |
SplitChip
| Field | Type | Meaning | Source |
|---|---|---|---|
left | Chip | The main chip. | L357 |
dragAndDropCommands | {…} (optional) | If set, the chip can be dragged. | L359 |
dragAndDropCommands.abandon | VSCodeCommand | Run when dropped on the garbage section. | L361 |
dragAndDropCommands.move | VSCodeCommand | Run when dropped on another commit, with that commit's hash appended. | L367 |
right | {…} (optional) | Optional second half. | L370 |
right.chip | Chip | The right-hand chip. | L371 |
right.borderColor | string | Border colour for the whole split chip, overriding both halves' borders. | L376 |
vscodeContext | {[key: string]: unknown} (optional) | Merged into data-vscode-context for the chip's context menu. If absent, no menu opens. | L383 |
ChipColors
| Field | Type | Meaning | Source |
|---|---|---|---|
background | T | Background colour. | L102 |
foreground | T | Text colour. | L103 |
border | T | Border colour. | L104 |
IconButton
| Field | Type | Meaning | Source |
|---|---|---|---|
command | VSCodeCommand | Command to run. Its title is ignored. | L92 |
icon | string | Codicon name, e.g. split-vertical. | L95 |
VSCodeCommand
| Field | Type | Meaning | Source |
|---|---|---|---|
title | string (optional) | Display title, e.g. New commit. | L112 |
command | string | Command ID, e.g. jj.new. | L114 |
tooltip | string (optional) | Hover text. | L115 |
arguments | unknown[] (optional) | Arguments passed to the command. Several call sites append extra arguments. | L116 |
File
| Field | Type | Meaning | Source |
|---|---|---|---|
path | string | Path. The type is exported but not used anywhere at this pin. | L390 |
Types that never cross the wire
The same file also defines the layout's own types. The webview builds them from the
Commits it receives, and the host never sees them. They're covered on
commit graph layout.