← Application platforms · Chapter 10

Connect commands, files, and task notes in a VS Code extension

Build project task notes by connecting command declarations, extension lifecycle, scoped storage, Workspace Trust, URI handling, and cancellation-safe writes.

After this guide, you can

Implement viewing and saving task notes, testing multi-root workspaces, Restricted Mode, canceled input, bounded writes, and disposable cleanup.

A task-note extension offers two actions: viewing project notes and appending a task entry. Because it reads and writes data, it must define project ownership, command availability, and behavior when an untrusted workspace opens.

Use synthetic task IDs without reading terminal history or executing project commands. Begin with native editor selection and input UI. A future webview would introduce a separate security boundary.

Connect declarations to handlers

Manifest contributions declare command IDs and visible titles; extension code registers their handlers. A menu entry does not implement behavior. Activation creates registrations and listeners, and deactivation releases them. Track disposables to prevent duplicate handling across lifecycle changes.

Activation behavior depends on contribution points and the minimum engines.vscode version. Follow current documentation instead of assuming every old activation declaration remains necessary. First check that commands appear, then that the intended functions run.

Keep command IDs stable while localizing display titles. Validate handler arguments because callers need not come through a menu. Unknown tasks or missing workspaces should produce clear explanations rather than writing into an arbitrary folder.

Define project and storage scope

A record contains task ID, text, time, and project identity. Multi-root workspaces need an explicit folder choice. Identically titled tasks in different projects must remain distinct. Use stable record identity and a defined ordering field.

Personal notes can use extension-provided workspace storage. Shared files should use an explicit export action. This lesson does not automatically modify repository files or store credentials in ordinary JSON. Separate global preferences from project data.

Represent resources as URIs. Remote workspaces and virtual filesystems may not map to local paths. Prefer workspace filesystem interfaces over passing URI paths into Node's local filesystem APIs. Clearly reject unsupported resource types when a feature is local-only.

Establish the development project

Prepare Node.js and Git and use the official VS Code Extension Generator to create a TypeScript project. Open src/extension.ts and press F5 to start an Extension Development Host. Verify the starter command. These are reader steps; the extension host has not been executed here.

Add viewing and saving commands to the manifest and register handlers on activation. Viewing reads the selected project's note storage and presents tasks. Saving selects project and task before accepting text. Canceling any step returns without empty records or subsequent writes.

Centralize reading, validation, persistence, and error mapping in a storage adapter. Check schema at startup. Preserve records on read errors. Use safe writes supported by the actual provider, and confirm saving only after it completes.

Enforce Workspace Trust in handlers

The official model supports full, limited, or no functionality in Restricted Mode. This extension chooses limited support: viewing works, while its side-effecting save path requires trust. This is an explicit project policy, not a universal requirement for every note extension.

Trust conditions can hide controls, but handlers must still check current trust because commands can be called directly. Workspace configuration can contain malicious execution flags or destinations. This extension exposes no shell execution command.

The Python model checks an allowlisted command policy. It runs no VS Code API or real trust transition.

def command_allowed(command, trusted):
    read_only = {"show_notes", "list_tasks"}
    if command in read_only:
        return True
    return trusted and command == "save_note"
assert command_allowed("show_notes", False)
assert not command_allowed("save_note", False)
assert command_allowed("save_note", True)
assert not command_allowed("run_shell", True)
print(command_allowed("save_note", False))

The output is False. Saving is denied in the restricted case, viewing is allowed, and an unknown shell command is denied even when trusted. The actual extension reads trust using documented APIs such as workspace.isTrusted, rather than trusting a Boolean supplied by a caller.

Handle paths, watchers, and overlapping writes

If export is added, ask the user for a destination, verify its permitted scope, and confirm replacement of existing files. Traversal and filesystem behavior complicate path validation; a simple string prefix is insufficient. Initially restrict writes to extension-owned storage.

Watchers can report the extension's own writes. Identify operations and separate external changes from save events to avoid endless rewriting loops. Debouncing can consolidate notifications, but the final read must reflect current data.

An older save response confirms its own snapshot only. Further edits remain unsaved. Multiple windows can write to the same project, requiring version or conflict handling rather than assuming one writer.

Explain errors and future UI boundaries

Missing workspaces prompt project selection. Read-only resources allow viewing with a save limitation. Preserve input after capacity or encoding failures. Treat cancellation as normal user behavior. Log error and operation IDs without full note content or private paths.

A future webview needs content security policy, restricted resource roots, and validated messages. Never turn note bodies into executable script. Webview messages do not automatically authorize privileged extension-host actions.

Define input size and encoding before writing. Decide whether blank notes are allowed; UI prompts do not enforce the rule. Explain timestamp provenance and do not use display time as a concurrency version. Keep task IDs separate from titles and order results deterministically.

Capture the workspace URI when selection begins and revalidate it after input. If the project closes or changes, stop saving and preserve copyable text rather than writing into another project. Unsupported provider operations produce controlled errors. Record the tested remote provider and leave untested environments explicitly unknown.

Exercise: verify project isolation

Use two teaching projects with same-named tasks and different notes. Switching projects must show only their own content. Test no workspace, canceled input, Restricted Mode, failed saving, and extension reload. Confirmed records remain and empty entries are not created.

Invoke a hidden save command directly and check enforcement. Deactivation should release registrations and listeners, and repeated startup should not duplicate events. Record untested remote behavior separately. Python proves the policy cases, not URI handling or extension lifecycle execution.

Sources & further reading

Sources checked on 2026-11-10. Original engineering explanation; examples are teaching data, not production measurements.