In short: VS Code activates the extension once startup has finished, or as soon as a workspace containing
.jj opens. activate creates an output-channel logger and installs it as the global
logger, then logs the version. If no workspace folder is open it stops there. Otherwise it builds the
jj subprocess client for the first workspace folder, wraps it in ApiImpl, and
builds JjUi with that API. JjUi registers the merge-conflict feature (tracker,
decorations, CodeLens and two commands), a read-only file system for the jj-commits scheme,
and then the commit graph webview view. Every
piece is a Disposable chained into context.subscriptions, so VS Code tears it
all down on deactivation. Errors go through one JjError type and a small set of
log… helpers, which the client and the file system providers now use.
1. package.json contributions
The manifest (source) is short: it declares one setting, one colour, two commands and one view. Field by field:
| Field | Value | Meaning |
|---|---|---|
name / displayName / publisher | jj-dojo / Jujutsu Dojo / google | Extension identity (source). |
version | 0.0.1 | (source) |
packageManager | pnpm@9.1.0 | The one place the pnpm version is set; Bazel and CI read it from here (source). |
engines.vscode | ^1.118.0 | Minimum VS Code version (source). |
extensionKind | ["workspace"] | Runs where the workspace is, for example on the remote side of a Remote-SSH session, where the repository and jj live (source). |
categories | ["SCM Providers"] | (source) |
activationEvents | onStartupFinished, workspaceContains:.jj | Activate after startup in any window, or right away when a folder containing .jj is opened (source). |
main | ./src/extension.js | The compiled entry point, built from src/extension.ts (source). |
contributes.configuration | jj.graph.focusMode: boolean, default false | The one setting: "In focus mode, the commit graph only shows commits on the current subgraph, and hides the rest of the commits." (source). At this pin only JjUiBase.dispose touches the module that reads it, to clear its cache; nothing reads the value yet (see gaps). |
contributes.colors | jj.mergeConflict.thirdSideBackground: light #ff800033, dark #ff983133 | The only themable colour: the background of a conflict's third side (source). It is used by the merge-conflict decorations, which are registered in activateMergeConflict. |
contributes.commands | jj.mergeConflict.acceptAllSides ("Accept all sides"), jj.mergeConflict.acceptOneSide ("Accept one side") | Invoked from the conflict CodeLens (source). |
contributes.menus | {} | No menu entries (source). |
contributes.views.scm | jj-dojo.commit-graph, name "JJ Graph", type: webview, visibility: visible | The commit graph, as a webview view inside the Source Control container (source). |
scripts | build, package, test, lint/fix/clean (gts), addlicense, dep:add/dep:update/dep:install | Thin wrappers over Bazel and gts; the dep:* scripts run Bazel's pinned pnpm (source). |
dependencies | @parcel/watcher, @vscode-elements/elements, @vscode/codicons, jsonc-parser, lit, safevalues | Runtime libraries: native file watching (used only by the file-system watcher, which nothing creates yet), webview components and icons, the icon-theme JSON parser, the webview UI framework, and safe HTML (source). |
2. activate and deactivate
activate does these things, in order:
- Creates an
OutputChannelLogger, pushes it ontocontext.subscriptions, and installs it withsetGlobalLogger. - Logs
Extension version: ${packageJSON.build}.package.jsonhas nobuildfield at this pin, and thevsixtarget packages it unchanged, so outside tests this readsundefined. The upstream test suppliesbuild: 'test-version'(test). - Reads the first workspace folder's path. If there is none, it logs
No workspace folders foundand returns, so nothing else is registered: no merge-conflict support and no graph view. - Creates a
SubprocessClientrooted at that folder and wraps it inApiImpl, the implementation of the UI-facingApiinterface. Its one method,readFileAtCommit, forwards to the client. - Constructs
JjUi(context, api)and pushes it ontocontext.subscriptions. - Logs
Extension activated successfully.
deactivate only removes the global logger. Everything
else is released by VS Code disposing context.subscriptions.
3. What ui.ts registers, and in which order
ui.ts splits the UI in two (source).
JjUiBase is "shared between external and internal" and keeps the Api it is given.
Its constructor calls activateMergeConflict(), then registers the commit file system.
JjUi adds the parts "only used externally": after super(api) it registers the
webview view provider. So merge-conflict support is always registered first.
| Order | Registration | Details |
|---|---|---|
| 1 | activateMergeConflict() | Creates a ConflictTracker, a ConflictDecorator and a ConflictCodeLensProvider on top of it, and registers the two jj.mergeConflict.* commands. It returns them as one vscode.Disposable.from(…) (source). |
| 2 | registerFileSystemProvider | A CommitIdFileSystemProvider over the API for the jj-commits scheme, with isCaseSensitive: true and isReadonly: true. Both the provider and its registration go into disposables (source). It serves a file's content at a commit, for URIs of the form jj-commits://<path>?commitId=…&opId=…&showParents (source); the read path is on roadmap. Nothing in the extension opens such a URI yet. |
| 3 | registerWebviewViewProvider | For CommitGraphViewProvider.viewType = jj-dojo.commit-graph (source), with webviewOptions.retainContextWhenHidden: true, so the graph keeps its state while the SCM view is hidden (source). How the view talks to its webview is on graph-protocol. |
4. Disposables and lifecycle
Every resource ends up in one tree of Disposables rooted at
context.subscriptions: the logger, and JjUi. JjUiBase keeps a
disposables array, and its dispose() first calls
removeFocusModeConfigCache(), then the shared helper
dispose. That helper disposes each entry in array
order, then empties the array, so a second dispose() is a no-op. The merge-conflict bundle
disposes its tracker, decorator, CodeLens provider and command registrations together. When
activation returned early for want of a workspace folder, only the logger is there to dispose.
5. Logging
| Piece | What it is | Source |
|---|---|---|
Logger | An interface with info(message) and error(message | Error, ...args). | logger.ts |
OutputChannelLogger | The only implementation. It is a log output channel named JJ Extension, created with {log: true}, which forwards to channel.info / channel.error. | output_channel_logger.ts |
| Global logger | A module-level variable. setGlobalLogger / removeGlobalLogger install and remove it, and every log… call is a no-op while none is installed. | logging.ts |
logInfo / logAndShowInfo | Log at info level. The second also shows an information notification. | logging.ts |
6. Error reporting
JjError is the project's single error type. It carries
four optional flags: isShownToUser, isLogged, isNetworkError and
isInternalError. getMessage() prefixes Internal Error - for
internal errors. addPrefix(p) rewrites the message to p: message.
JjError.from(x) normalises anything thrown: a JjError as is, a string, an
Error (message only), or JSON.stringify of anything else. A private
prefixes array is declared but never used, since addPrefix edits the message
directly. isNetworkError isn't set anywhere at this pin.
| Helper | Behaviour | Source |
|---|---|---|
logError(e) | Normalises to JjError. If it isn't logged yet, marks it and calls logger.error(error, extra). extra is the global error callback's result, passed as a separate argument rather than appended to the message (the test expects ['ERROR', 'some error', 'extra info']). | logging.ts, test |
logAndShowUserError(e) | logError, then, once per error, showErrorMessage(getMessage()). | logging.ts |
logAndShowInternalError(e) | Marks the error internal, logs it, and once per error hands it to the global feedback provider. It marks isShownToUser whether or not a provider is registered, and doesn't call showErrorMessage itself. | logging.ts |
| Error callback | setGlobalErrorCallback(() => string). Its result is attached to every logged error. | error_callback.ts |
| Feedback provider | setGlobalFeedbackProvider((error) => void). It is invoked for internal errors and may, per its comment, "pop up a dialog to the user asking to file a bug". | feedback.ts |
extension.ts only uses logInfo. Of the wired code, the subprocess client and
the file system providers throw through logAndShowInternalError: for a failed
jj template parse, an unknown file status, a malformed jj-commits URI, or a
write to the read-only file system (see roadmap).
Nothing calls the two setGlobal… hooks, so no error callback or feedback provider is
installed. The machinery is tested in logging_test.ts.