Chapter 12 — The Extension Host UI
The Debug80 sidebar panel is a VS Code WebviewView embedded in the built-in Run & Debug container ("views": { "debug": [...] } in package.json). It runs in a separate JavaScript context from the extension host and communicates with it entirely through message passing. The extension host side of this boundary is managed by PlatformViewProvider in src/extension/platform-view-provider.ts.
The view is registered under "views": { "debug": [...] } in package.json, which places it as a collapsible subpanel alongside Variables, Watch, Call Stack, and Breakpoints. The extension activates as soon as the user expands the panel, via the "onView:debug80.platformView" activation event.
This chapter covers the provider class: what state it holds, how the webview is created and destroyed, the complete message catalogue in both directions, how message routing is structured, and how the provider wires together the debug adapter, the workspace, and the UI.
Language contributions
Debug80 also owns the editor-facing language contribution for Z80 assembly. In package.json, the extension contributes:
- language id
z80-asmfor.asm,.z80,.a80, and.s - language id
z80-lstfor.lst - breakpoint support for common Z80 assembly language ids and listings
- TextMate grammars at
syntaxes/z80-asm.tmLanguage.jsonandsyntaxes/z80-lst.tmLanguage.json
The Z80 assembly grammar is deliberately lexical. It recognizes semicolon comments, strings, labels, local labels, directives, condition-bearing control instructions, Z80 mnemonics, registers, condition codes, number formats, symbols, operators, and routine-comment annotations such as @routine, @in, @out, @clobbers, and @preserves.
Colour is driven by default editor.tokenColorCustomizations in package.json. The current palette distinguishes comments, labels, symbols, directives, annotations, instructions, registers, conditions and flags, strings, function names, operators, and numeric literals. This is TextMate colouring, not semantic-token analysis.
registerLanguageAssociations() in src/extension/language-association.ts is a safety net for opened documents. Once VS Code knows the contributed languages, it assigns .asm, .z80, .a80, and .s documents to z80-asm for file and untitled documents. That keeps decorations and breakpoints aligned with the contributed language ids even when a file was opened before associations settled.
The language-server design note in the source repository is still a future direction. The current implementation provides TextMate highlighting and file association, not LSP diagnostics, completion, hover, or semantic tokens.
The provider class
PlatformViewProvider implements vscode.WebviewViewProvider. VS Code calls resolveWebviewView() once when the sidebar panel first becomes visible. After that, the provider drives the webview by posting messages to it and handling messages it sends back.
export class PlatformViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'debug80.platformView';
private view: vscode.WebviewView | undefined;
private currentPlatform: PlatformId | undefined;
private currentSession: vscode.DebugSession | undefined;
private currentSessionId: string | undefined;
private uiRevision = 0;
private selectedWorkspace: vscode.WorkspaceFolder | undefined;
private hasProject = false;
private readonly workspaceState: vscode.Memento | undefined;
private readonly extensionUri: vscode.Uri;
...
}
Platform module cache — loadedModules
The provider maintains a second parallel map: loadedModules: Map<string, PlatformUiModules>. Where platformStates holds runtime data (hardware state, serial buffer, tab), loadedModules holds behaviour — the functions that know how to generate HTML, serialize state to update messages, and handle incoming webview messages.
Modules are loaded through loadPlatformUi() and cached in loadedModules. In the current provider implementation that loading is done eagerly by preloadAllPlatforms() during resolveWebviewView(), so each registered platform has both its module bundle and state entry ready before the first render. After a module is loaded, initPlatformState() creates the corresponding PerPlatformState entry.
The getActiveBundle(id) helper retrieves both maps together, returning { modules, state } or undefined. This ensures the two maps stay synchronized — callers never work with state that has no matching modules, or vice versa.
The PlatformUiModules contract
PlatformUiModules in src/extension/platform-view-manifest.ts is the interface every platform UI must satisfy:
interface PlatformUiModules<TUiState = unknown> {
getHtml(tab: PanelTab, webview: vscode.Webview, extensionUri: vscode.Uri): string;
createUiState(): TUiState;
resetUiState(state: TUiState): void;
applyUpdate(state: TUiState, payload: unknown): Record<string, unknown>;
createMemoryViewState(): MemoryViewState;
handleMessage(message: PlatformViewMessage, context: PlatformUiMessageContext): Promise<void>;
buildUpdateMessage(state: TUiState, uiRevision: number): Record<string, unknown>;
buildClearMessage(state: TUiState, uiRevision: number): Record<string, unknown>;
snapshotCommand: 'debug80/tec1MemorySnapshot' | 'debug80/tec1gMemorySnapshot';
}
| Method | Role |
|---|---|
getHtml |
Build the webview HTML for a given initial tab and webview context |
createUiState |
Allocate a blank TUiState (called once per platform on module load) |
resetUiState |
Zero out UI state on session end (called by clear()) |
applyUpdate |
Merge an incoming hardware update payload into TUiState; return the serialized message fields |
createMemoryViewState |
Create a fresh MemoryViewState for the memory inspector |
handleMessage |
Dispatch a platform-specific webview message (e.g. key, matrixKey, refresh) |
buildUpdateMessage |
Serialize the current TUiState into a full update message (used for rehydration) |
buildClearMessage |
Serialize a zeroed TUiState into a clear update message (session ended) |
snapshotCommand |
The DAP custom request name to use when fetching a memory snapshot |
Each platform wires its modules through a PlatformUiEntry factory (createTec1PlatformUiEntry(), etc.) in src/extension/platform-ui-entries.ts. The factory’s loadUiModules() function uses dynamic import() to pull in the platform’s HTML, memory, message-handling, and state modules, then composes them into a single PlatformUiModules object.
Parallel state trees
The provider maintains a platformStates: Map<string, PerPlatformState> with an entry for every registered platform (Simple, TEC-1, TEC-1G). All three are held in memory simultaneously. Each entry holds:
uiState— platform-specific hardware state (digits, matrix rows, LCD buffer, speaker, etc.; empty for Simple)serialBuffer— accumulated serial / terminal output text (max 8 000 characters)memoryViews— which memory regions are shown in the memory inspectoractiveTab— which tab ('ui'or'memory') was last activerefreshController— memory snapshot polling machinery
Only one platform is active at a time (currentPlatform), but all other platforms’ state is preserved. If the user switches from TEC-1 to TEC-1G and back, the TEC-1 state rehydrates without a round trip to the adapter.
The uiRevision counter
Every update message from the extension host carries a uiRevision number. The webview tracks the last revision it applied and ignores any message with a lower number. This prevents a race condition where a slow-arriving update from a previous session overwrites a fresh update from the current one.
The counter only ever increases. nextUiRevision() increments uiRevision and returns the new value; it is called each time a state-bearing message is sent. The counter is never reset — not even when renderCurrentView() is called. When renderCurrentView() sets new HTML on the webview, the webview’s own JavaScript state (including its local uiRevision tracking) resets to zero because the webview frame is destroyed and recreated. The extension host counter continues to increase from wherever it was, so the first update message sent after a render will have a revision number higher than zero and the freshly initialised webview will accept it.
HTML generation
The webview’s HTML is built from a template in src/platforms/panel-html.ts. buildPanelHtml() reads the platform’s index.html template from the built webview directory, resolves URI references for scripts and stylesheets, generates a CSP nonce, and replaces template tokens:
| Token | Replaced with |
|---|---|
| `` | webview.cspSource — the allowed content source |
| `` | Random nonce for inline script/style |
| `` | Platform-specific styles.css URI |
| `` | Shared webview/common/styles.css URI |
| `` | Compiled index.js URI |
| `` | 'ui' or 'memory' — which tab opens first |
The HTML is set on webviewView.webview.html. Setting this property destroys the existing webview content and starts fresh — all JavaScript state in the webview is lost. This happens on every platform switch and every time the sidebar is revealed after being hidden.
Webview resources are restricted to out/webview/ via localResourceRoots. No other paths are accessible.
resolveWebviewView()
This is the provider’s main lifecycle method. VS Code calls it once, providing the WebviewView object. The method:
- Stores the view reference.
- Configures the webview options (scripts enabled, resource roots).
- Registers the message handler.
- Registers visibility and disposal handlers.
- Calls
renderCurrentView(false)to populate the initial HTML and state.
Visibility handler
When the sidebar becomes hidden, the memory refresh polling stops. When it becomes visible again, renderCurrentView(true) runs — the HTML is rebuilt, state is reposted, and memory polling restarts if the memory tab is active.
This means every show/hide cycle regenerates the webview HTML. The buffered state (serial text, UI state) is reposted from the provider’s in-memory copies, so the user sees a consistent panel despite the HTML being recreated.
renderCurrentView()
This is the method that actually populates the panel. It runs on every platform switch and every sidebar reveal:
1. Set webview.html to platform HTML (destroys old webview)
2. Post projectStatus (workspace roots, targets, selected target, active platform)
3. Post sessionStatus (running/paused/not running)
4. Post full update snapshot (hardware state for TEC-1/TEC-1G; empty for Simple)
5. For TEC-1G: merge and post `uiVisibility` (defaults → launch `tec1g.uiVisibility` if any → workspace Memento per target) with `persist: true` so the webview and `getState` stay aligned
6. Post serialInit (full buffered serial/terminal text, if any)
7. Post selectTab (active tab)
8. Sync memory refresh (start polling if on memory tab)
mergeAndPostTec1gPanelVisibility() is also invoked when refreshProjectStatus() reposts project metadata (e.g. after target change) and after setTec1gAdapterVisibility() runs from the debug80/platform custom event, but it is not tied to every lightweight postProjectStatus() (such as stop-on-entry toggles) to avoid spuriously overwriting the in-flight webview.
When no debug session is active, the provider still renders a platform webview so the project header and setup card remain interactive. The selected platform identity comes from the remembered workspace/project state rather than from an active runtime.
Message routing
Inbound messages from the webview are typed as PlatformViewInboundMessage — a union defined in src/contracts/platform-view.ts. The raw message is immediately handed to handlePlatformViewMessage() in src/extension/platform-view-messages.ts, which is responsible for all routing decisions.
platform-view-messages.ts
handlePlatformViewMessage() receives the message and a dependency object (PlatformViewMessageDependencies) that provides callback functions for each action. It dispatches on msg.type:
- Project and session commands (
createProject,selectProject,openWorkspaceFolder,configureProject,selectTarget,restartDebug,setEntrySource,startDebug) are forwarded to the corresponding callback, which invokes a VS Code command. ThecreateProjectpath passes an optionalplatformstring todebug80.createProject; when present the command resolves the default kit for that platform viagetDefaultProjectKitForPlatform()and scaffolds without showing additional pickers.setStopOnEntryis handled inline by the provider — it updatesthis.stopOnEntryand refreshes theprojectStatuspayload. saveTec1gPanelVisibility(TEC-1G) carriesvisibilityand optionaltargetNameand is handled by the provider before the platform-adapter path — it updatesworkspaceStateunderdebug80.tec1g.uiVisibilityByTargetand does not require an activez80session.- Serial commands (
serialSendFile,serialSave) are forwarded to their callbacks. serialClearcallsclearSerialBufferfor the current platform.- Any unrecognised type falls through to
handlePlatformMessage, which dispatches to the platform-specific adapter.
This function contains no provider state — it is a pure routing layer, which makes it independently testable.
Serial actions — platform-view-serial-actions.ts
handlePlatformSerialSendFile() and handlePlatformSerialSave() in src/extension/platform-view-serial-actions.ts handle the two serial file workflows:
handlePlatformSerialSendFile()opens a file picker, reads the file, and sends each character individually as adebug80/tec1SerialInputordebug80/tec1gSerialInputcustom request (2 ms between characters, 10 ms between lines, CR appended at each line end). A cancellable progress notification shows during the transfer.handlePlatformSerialSave()opens a save dialog and writes the buffered serial text to a file. If the contents look like Intel HEX (all lines start with:), the default filter offers.hex.
Platform module dispatch
Platform-specific messages (hardware input, memory inspector, tab changes) are routed through the PlatformUiModules object retrieved from loadedModules. Calling modules.handleMessage(msg, context) delegates to the platform’s own message handler, which translates webview messages into debug adapter custom requests. There is no platform-specific branching in platform-view-messages.ts itself — all platform variation lives behind the PlatformUiModules interface.
Messages: extension host → webview
All messages are posted via postMessage(). The webview handles them in window.addEventListener('message', ...).
| Type | Key fields | When sent |
|---|---|---|
update |
uiRevision, platform-specific hardware fields (empty object for Simple) |
Platform hardware state changes |
serial |
text: string |
Incremental serial/terminal data |
serialInit |
text: string |
Full serial/terminal buffer on rehydration |
serialClear |
— | Clear the serial/terminal display |
projectStatus |
roots[], targets[], rootName?, rootPath?, projectState?, hasProject?, targetName?, entrySource?, platform?, stopOnEntry? |
Workspace or project state changes |
sessionStatus |
status: 'starting' \| 'running' \| 'paused' \| 'not running' |
Debug session state changes |
selectTab |
tab: 'ui' \| 'memory' |
Tab should be selected |
snapshot |
Register and memory dump | Memory inspector refresh completes |
snapshotError |
message?: string |
Memory snapshot request failed |
uiVisibility |
visibility: Record<string, boolean>, persist: boolean |
TEC-1G section visibility (merged payload; persist: true also mirrors into webview setState when the controller applies the message) |
The TEC-1G update message carries additional fields not present in TEC-1:
{
type: 'update',
uiRevision: number,
digits: number[],
// RGB matrix — three colour planes
matrix: number[], // red plane rows
matrixGreen?: number[],
matrixBlue?: number[],
matrixBrightnessR?: number[], // per-pixel brightness (64 entries)
matrixBrightnessG?: number[],
matrixBrightnessB?: number[],
matrixMode?: boolean,
// GLCD
glcd: number[], // 1024-byte GDRAM
glcdDdram?: number[],
glcdState?: { displayOn, graphicsOn, cursorOn, ... },
// Text LCD
lcd: number[],
lcdState?: { displayOn, cursorOn, cursorAddr, ... },
lcdCgram?: number[],
// System
speaker: number,
speedMode: 'fast' | 'slow',
sysCtrl?: number,
bankA14?: boolean,
capsLock?: boolean,
speakerHz?: number
}
Messages: webview → extension host
These arrive in onDidReceiveMessage and are dispatched as described above.
| Type | Key fields | Handler |
|---|---|---|
startDebug |
— | Execute start debug command |
createProject |
rootPath, platform? |
Execute create project command; optional platform pre-selects kit filter |
openWorkspaceFolder |
— | Execute open folder command |
selectProject |
rootPath |
Execute workspace selection |
configureProject |
— | No-op (config is now done via project header controls) |
saveProjectConfig |
platform: string |
Write projectPlatform + per-target platform to debug80.json, then restart debug. This is now effectively a legacy path because the platform selector is hidden once a project is initialized. |
selectTarget |
rootPath, targetName |
Execute target selection |
restartDebug |
— | Execute restart debug command |
setEntrySource |
— | Execute set entry source command |
serialSendFile |
— | File picker → character-by-character send (TEC-1/TEC-1G) |
serialSave |
text |
Save dialog → write file |
serialClear |
— | Clear serial/terminal buffer |
saveTec1gPanelVisibility |
visibility, optional targetName |
Provider persists TEC-1G section toggles to workspace Memento (see platform-view-messages bullet above) |
key |
code: number |
Platform adapter → adapter custom request |
reset |
— | Platform adapter → adapter custom request |
speed |
mode |
Platform adapter → adapter custom request |
tab |
tab |
Update active tab; start/stop memory polling |
refresh |
memory params | Platform adapter → fetch memory snapshot |
registerEdit |
register, value |
Platform adapter → adapter custom request |
memoryEdit |
address, value |
Platform adapter → adapter custom request |
matrixKey |
key, pressed, modifiers |
TEC-1G only; platform adapter |
matrixMode |
enabled |
TEC-1G only; platform adapter |
Platform message routing
When a platform-specific message arrives, resolvePlatformAdapter() returns the appropriate adapter and adapter.handleMessage() delegates to handleTec1Message() or handleTec1gMessage(). These functions translate webview messages into debug adapter custom requests:
| Webview message | Adapter custom request |
|---|---|
key (TEC-1) |
debug80/tec1Key |
reset (TEC-1) |
debug80/tec1Reset |
speed (TEC-1) |
debug80/tec1Speed |
refresh (TEC-1) |
debug80/tec1MemorySnapshot |
registerEdit |
debug80/registerWrite |
memoryEdit |
debug80/memoryWrite |
key (TEC-1G) |
debug80/tec1gKey |
matrixKey (TEC-1G) |
debug80/tec1gMatrixKey |
matrixMode (TEC-1G) |
debug80/tec1gMatrixMode |
reset (TEC-1G) |
debug80/tec1gReset |
speed (TEC-1G) |
debug80/tec1gSpeed |
refresh (TEC-1G) |
debug80/tec1gMemorySnapshot |
All adapter requests go through session.customRequest() on the current vscode.DebugSession.
Shared message contract — src/contracts/platform-view.ts
The types shared between the extension host and webview are defined in src/contracts/platform-view.ts:
PlatformId—'simple' | 'tec1' | 'tec1g'; the canonical platform identifier used throughout the extension and webview.ProjectStatusPayload— the shape of theprojectStatusmessage body. The key field isprojectState?: 'noWorkspace' | 'uninitialized' | 'initialized', which drives control visibility in the webview. Other fields:roots[],targets[],rootName,rootPath,hasProject(legacy compat),targetName,entrySource,platform, andstopOnEntry(the current global stop-on-entry toggle value).PlatformViewControlMessage— a discriminated union of all project/session/serial control messages (startDebug,restartDebug,createProject,openWorkspaceFolder,selectProject,configureProject,saveProjectConfig,setStopOnEntry,selectTarget,setEntrySource,serialSendFile,serialSave,serialClear,saveTec1gPanelVisibility). ThesaveProjectConfigmessage carries{ platform: string }and triggers a config write + debug restart. ThecreateProjectmessage carries an optionalplatform?: stringfield that, when present, selects the default kit for that platform without showing pickers. ThesetStopOnEntrymessage carries{ stopOnEntry: boolean }and updates the provider’s global toggle. ThesaveTec1gPanelVisibilitymessage carries the full section visibility object and an optionaltargetNameto key workspace persistence.PlatformViewInboundMessage— the full union of all messages the extension host can receive:PlatformViewControlMessage | Tec1Message | Tec1gMessage | { type?: string; [key: string]: unknown }.
This file is the authoritative definition of the message boundary. Platform-view-messages.ts imports PlatformViewInboundMessage directly from it.
Platform registration model — src/extension/platform-extension-model.ts
registerExtensionPlatform() in src/extension/platform-extension-model.ts is the unified API for registering a platform with both the runtime provider registry and the extension UI:
interface ExtensionPlatformEntry {
runtime: PlatformManifestEntry;
ui?: PlatformUiEntry;
}
function registerExtensionPlatform(entry: ExtensionPlatformEntry): void;
A single call registers the platform’s runtime (via registerPlatform() in src/platforms/provider.ts) and, if a ui entry is provided, its sidebar UI (via registerPlatformUi() in platform-view-manifest.ts). listExtensionPlatforms() returns the unified list in runtime-manifest order, merging any UI entries that are present. registerRuntimePlatform() is a compatibility alias for registering runtime-only platforms using the existing public surface.
Memory refresh
The memory inspector polls the adapter for live register and memory snapshots while the session is paused. The refresh controller (tec1RefreshController, tec1gRefreshController) manages this polling:
- Start polling when the memory tab becomes active.
- Stop polling when the panel is hidden or the tab switches away.
- Poll at 150 ms intervals.
- On each poll, call
session.customRequest('debug80/tec1MemorySnapshot', snapshotPayload)and post the result as asnapshotmessage.
The snapshot payload describes which memory regions and views are currently displayed. The adapter reads those regions from the Z80 memory array and returns them in a single response.
Project status and the three-state model
getProjectStatusPayload() assembles the project header data and signals one of three explicit project states via the projectState field of ProjectStatusPayload.
projectState values
| Value | Condition | What the provider includes |
|---|---|---|
'noWorkspace' |
No workspace folder selected | roots[] only |
'uninitialized' |
Folder selected, no debug80.json found |
rootName, rootPath, platform (current or 'simple'), stopOnEntry |
'initialized' |
Folder has a debug80.json |
Full payload: targets[], targetName, platform, stopOnEntry, entrySource |
The 'uninitialized' branch includes platform: this.currentPlatform ?? 'simple' so the webview can show the Platform selector at its correct value even before a project config exists.
Webview state helpers — webview/common/
Three shared modules translate the payload into rendering decisions:
project-state.ts — exports ProjectViewState = 'noWorkspace' | 'uninitialized' | 'initialized' and resolveProjectViewState(payload). The function prefers the explicit projectState field when present, falling back to inferring state from hasProject and rootPath for backward compatibility.
setup-card-state.ts — exports resolveSetupCardState(selectedRoot, projectState, targetCount): SetupCardState | null. Returns the text and primary-action label for the onboarding card, or null to hide it:
noWorkspace/ no selected root → “No workspace folder is open” + Open Folder buttonuninitialized→ “Uninitialized Debug80 project” + Initialize Project buttoninitialized→null(card hidden)
project-controls.ts — exports applyInitializedProjectControls(payload, elements). Enforces which controls are visible based on projectState:
| Control | noWorkspace |
uninitialized |
initialized |
|---|---|---|---|
| Platform selector | hidden | visible | hidden |
platformInfoControl |
hidden | hidden | hidden |
| Target control | hidden | hidden | visible |
| Stop on entry label | hidden | hidden | visible |
| Restart button | hidden | hidden | visible |
| Tabs (UI / Memory) | hidden | hidden | visible |
| Panel content areas | hidden | hidden | visible |
The Platform selector is intentionally shown in the uninitialized state so the user can choose a platform before clicking Initialize Project. Once the project exists, the platform is stored in debug80.json and the selector is hidden. The platformInfoControl element (a read-only platform label) is always hidden — it exists in the HTML for legacy reasons but is never made visible by applyInitializedProjectControls.
Because VS Code webview stylesheets set display: flex on .project-control elements, the UA [hidden] → display: none rule is overridden. webview/common/styles.css includes an explicit .project-control[hidden] { display: none } rule, and likewise .stop-on-entry-label[hidden] { display: none }, to ensure hidden controls are correctly invisible.
Project scaffolding and project kits
When debug80.createProject runs for a new workspace, it calls scaffoldProject() in src/extension/project-scaffolding.ts. The core abstraction that replaced the old ad-hoc platform field is the project kit (src/extension/project-kits.ts).
ProjectKit
A ProjectKit is an immutable descriptor for one platform/profile combination:
type ProjectKit = {
id: ProjectKitId; // e.g. 'tec1/mon1b'
platform: ScaffoldPlatform; // 'simple' | 'tec1' | 'tec1g'
profileName: string; // profile key written into debug80.json
label: string; // shown in the quick-pick
description: string;
appStart: number;
entry: number;
starterTemplates: Record<StarterLanguage, string>; // relative template paths
bundledProfile?: {
bundleRelPath: string; // versioned bundle dir path, e.g. 'tec1/mon1b/v1'
romPath: string; // workspace-relative ROM destination
listingPath?: string;
sourceRoots: string[];
};
};
The four built-in kits are:
| Kit ID | Platform | Profile | Monitor |
|---|---|---|---|
simple/default |
simple |
default |
none |
tec1/classic-2k |
tec1 |
classic-2k |
none |
tec1/mon1b |
tec1 |
mon1b |
MON-1B (bundled) |
tec1g/mon3 |
tec1g |
mon3 |
MON3 (bundled) |
Kit selection — getDefaultProjectKitForPlatform() and chooseProjectKit()
project-kits.ts exports two selection paths:
getDefaultProjectKitForPlatform(platform) — returns the single default kit for a platform string ('simple', 'tec1', or 'tec1g') without showing any picker. This is the path taken when the user clicks Initialize Project from the panel’s uninitialized state: the platform value already shown in the Platform selector is passed directly, and the project is scaffolded immediately using the platform’s bundle-first default kit (TEC-1 → tec1/mon1b; TEC-1G → tec1g/mon3; Simple → simple/default). In this path the scaffold does not ask for target name or source language; it creates src/main.asm and derives the initial target name from that file.
chooseProjectKit(preselectedPlatform?) — the interactive path. Calls getProjectKitChoices() / listProjectKits(). If only one kit matches the platform, the picker is skipped. Otherwise a showQuickPick() prompt lists all matching kits. This path is used when the command is invoked without a pre-selected platform (e.g. from the Command Palette).
The platform? field on the createProject webview message flows through handleCreateProject in platform-view-messages.ts into the debug80.createProject command args and then into scaffoldProject().
buildScaffoldPlan() — interactive plan construction
After kit selection, buildScaffoldPlan() collects the remaining inputs:
- A target name (input box, default
'app'). - A source file choice: an existing
.asmfile from the workspace, or a new assembly starter file.
The result is a ScaffoldPlan — { kit, targetName, sourceFile, outputDir, artifactBase, starterLanguage?, starterFile? }.
createDefaultProjectConfig() — writing debug80.json
createDefaultProjectConfig(plan) assembles the debug80.json structure from the plan:
- A
profilessection with one entry (plan.kit.profileName) containing the platform, description, and — if the kit has abundledProfile— abundledAssetsmap withromHexand optionallylistingentries (each aBundledAssetReference). - A
targetssection with one entry (plan.targetName) containingsourceFile,outputDir,artifactBase,platform,profile, and the platform-specific memory map block (simple,tec1, ortec1g). For kits with abundledProfile, the target block also includesromHex, optionalextraListings, andsourceRoots. - Top-level
projectVersion,projectPlatform,defaultProfile, anddefaultTargetfields.
When the scaffold creates new files in this pass (debug80.json and/or a new .vscode/launch.json), it also calls ensureDebug80Gitignore() in src/extension/project-gitignore.ts to create or append a standard Debug80-marked ignore block (see Chapter 2).
Bundled ROM files are not copied during scaffolding. The generated profile records BundledAssetReference entries. During launch, resolveBundledAssetRuntimePath() in src/debug/launch-args.ts checks the workspace path first; if that path is missing and corresponds to the bundled asset destination, it falls back to the copy inside the extension bundle. The explicit debug80.materializeBundledRom command copies local files when the user wants workspace copies.
Starter templates
If the user chose to create a starter source file, createStarterSourceContent() reads the template from resources/project-kits/<kit.starterTemplates[language]> via readProjectKitStarterTemplate(). The file is written to the workspace before debug80.json is created. If the file already exists, it is not overwritten.
Summary
-
PlatformViewProvideris the single point of contact between the debug adapter and the webview. It holds all hardware display state and serial/terminal buffers in memory on the extension host side for all three platforms simultaneously. -
The Debug80 panel is registered under
"views": { "debug": [...] }inpackage.json, placing it as a collapsible subpanel in the Run & Debug sidebar. The"onView:debug80.platformView"activation event ensures the extension starts as soon as the user expands the panel. -
The panel uses a responsive two-column layout driven by CSS container queries on
#app. Below 440 px (TEC-1) or 480 px (TEC-1G) the layout collapses to a single column; above those thresholds the original two-column grid is restored. -
The webview HTML is regenerated on every platform switch and sidebar reveal. Buffered state is reposted from in-memory copies, making the panel self-restoring.
-
The
uiRevisioncounter only ever increments — it is never reset, not even when the webview HTML is replaced. The webview’s own counter resets when new HTML is set; the host counter does not. -
Message routing is handled by
handlePlatformViewMessage()inplatform-view-messages.ts. Serial file workflows are inplatform-view-serial-actions.ts. Platform-specific dispatch callsmodules.handleMessage()via thePlatformUiModulesinterface — no platform branching in the routing layer. -
ProjectStatusPayloadcarries aprojectStatefield with three explicit values:'noWorkspace','uninitialized', and'initialized'. The webview usesresolveProjectViewState()fromwebview/common/project-state.tsto map this to rendering decisions.applyInitializedProjectControls()fromwebview/common/project-controls.tsshows or hides each control depending on the state — the Platform selector is visible only while uninitialized; all debug controls are visible only once initialized. -
When
projectStateis'uninitialized', the setup card shows an Initialize Project button. Clicking it sendscreateProjectwith the platform currently shown in the Platform selector. The extension usesgetDefaultProjectKitForPlatform()to resolve the default kit and scaffolds the project without displaying any pickers. -
Stop on entry is a global session toggle held as
public stopOnEntry = falseonPlatformViewProvider— not stored indebug80.jsonand not persisted across restarts. It defaults tofalse. When the checkbox changes, the provider updates the field and broadcasts aprojectStatusrefresh; the new value applies on the next explicit restart (it does not trigger an automatic restart). At every call tostartCurrentProjectDebugging(),platformViewProvider.stopOnEntryis passed directly in the launch config object, taking priority over anystopOnEntryvalue left in the project config. -
Restart semantics for workspace changes:
debug80.selectWorkspaceFolderrestarts the active debug session whenever the selected project config path changes — regardless of whether the platform changed. Previously the restart was only triggered on a platform change. -
The provider holds two parallel maps:
platformStates(hardware state, serial buffer, tab) andloadedModules(behaviour — HTML generation, state serialization, message handling). Modules are loaded once at startup viapreloadAllPlatforms()and cached permanently.PlatformUiModulesinplatform-view-manifest.tsis the interface every platform UI must satisfy. -
The shared message contract is defined in
src/contracts/platform-view.ts:PlatformId,ProjectStatusPayload(includesprojectState,platform,stopOnEntry),PlatformViewControlMessage(includessetStopOnEntry,saveProjectConfig,createProjectwith optionalplatform?,saveTec1gPanelVisibilityfor TEC-1G section persistence), andPlatformViewInboundMessage. -
registerExtensionPlatform()inplatform-extension-model.tsis the unified API for registering both the runtime and UI concerns of a platform in a single call. -
The memory refresh controller polls the adapter at 150 ms intervals when the memory tab is active and the panel is visible. Polling stops automatically on tab switch or panel hide.
-
Project status is assembled from workspace folders,
debug80.jsondiscovery, workspace-persisted target selection, and the active platform ID. It emits one of threeprojectStatevalues and drives the project header (Project button ++Add-folder button, Target dropdown, Platform dropdown, Stop-on-entry checkbox, Restart button). -
Project scaffolding is driven by project kits (
src/extension/project-kits.ts). A kit packages the platform, profile name, memory-map defaults, starter templates, and optional bundled ROM references into a single descriptor.buildScaffoldPlan()selects a kit interactively (command palette path);getDefaultProjectKitForPlatform()selects the bundle-first default silently (panel initialization path).createDefaultProjectConfig()writesprofilesandtargetsfrom the chosen kit. Bundled ROM references resolve to workspace files first, then extension-bundled files;debug80.materializeBundledRominstalls workspace copies on demand.