Last reviewed: November 11, 2026 (Asia/Shanghai).
Direct answer
Build a CometAPI VS Code extension with a CometAPI: Summarize Selection command. Capture the highlighted text as soon as the command starts. In live mode, ask before sending that captured text to CometAPI’s documented Chat Completions route , read the key from VS Code SecretStorage, and open the completed reply in an untitled plain text editor beside the original. Keep the source document untouched so the reader can compare the summary with what they selected.
Start with the included fixture mode. It exercises selection checks and opens the same result view without reading a key or sending a request. Switch to a live model only after that path works.
Who this is for
This guide is for JavaScript developers building a local command for VS Code desktop. Use a local window and a small development folder. The example targets VS Code 1.105.0 or a later compatible 1.x release, and the live branch checks for fetch and AbortSignal.timeout in the extension host. It uses plain CommonJS, so there is no build step or SDK installation. Run it through VS Code, which supplies the vscode module.
The command is designed for a single selection and a single text reply. It takes no file paths or workspace attachments and has no tool loop. Use an invented passage for your first check, so you can repeat it without sending project material.
Key takeaways
- Read the selection before the first asynchronous step. A confirmation dialog or secret lookup should never change which text the command will send.
- Keep the API key in
context.secretsand the nonsecret model ID in the extension code. The workspace cannot supply a different destination through settings. - Run the fixture before adding a credential. It checks the editor workflow without making a generation request.
- Treat a usable reply as a separate check from HTTP success. Reject truncation, refusals, and tool calls for this command.
- Open the result as plain text in a new editor so the person running the command decides what to save or copy.
Contract details to verify
The VS Code command guide
explains that the Command Palette does not pass a text editor or selection to your handler. The handler reads window.activeTextEditor itself and uses document.getText(editor.selection) to capture one range. The pinned VS Code API declarations
define these editor methods and the openTextDocument({ language, content }) overload that creates an untitled document.
The CometAPI Chat Completions reference
defines the HTTPS route, Bearer authentication, and JSON message format. The command sends a short instruction and the captured passage as one user message with stream: false. It accepts one assistant choice with a stop finish reason and nonblank text. That is the example’s acceptance rule; the API also documents other outcomes, including length, content_filter, and tool_calls.
| Boundary | Example behavior |
|---|---|
| Input | One nonblank selection, at most 4,096 UTF-8 bytes of selected text |
| Destination | Fixed HTTPS Chat Completions URL; Fetch rejects redirects |
| Dispatch | One request after Send; a second invocation in the same extension instance waits for the first to finish before it can run again |
| Wait | A 30-second local abort signal passed to Fetch |
| Output | One completed text choice in an untitled plain text editor |
The byte check limits the selected text. The instruction and JSON framing add bytes, and model tokens use a different unit. Asking for two sentences is an instruction, not an enforced output limit. The small local example also reads the response with response.json(), which buffers the body; the input check does not limit that download. The Node.js globals reference
documents Fetch and the timeout signal. The 30-second value is a local example, not a statement about model latency or remote cancellation.
Step 1: Add the extension manifest and launcher
Create an empty local folder and open it in VS Code. Add package.json:
{
"name": "cometapi-selection-summary",
"displayName": "CometAPI Selection Summary",
"publisher": "local-demo",
"version": "0.0.1",
"engines": { "vscode": "^1.105.0" },
"main": "./extension.js",
"contributes": {
"commands": [
{ "command": "selectionSummary.saveKey", "title": "CometAPI: Save API Key" },
{ "command": "selectionSummary.forgetKey", "title": "CometAPI: Forget API Key" },
{ "command": "selectionSummary.summarize", "title": "CometAPI: Summarize Selection" }
]
}
}
The manifest supplies the Command Palette labels and IDs. The handler registration in the next step supplies the behavior. VS Code activates an extension for its contributed commands on invocation at this engine version, so this manifest can omit explicit onCommand entries.
Create .vscode/launch.json in the same folder:
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Selection Summary",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"]
}
]
}
Step 2: Register the commands and keep the reply separate
Add extension.js beside package.json. Leave FIXTURE_MODE set to true for the first run.
const vscode = require('vscode');
const FIXTURE_MODE = true;
const MODEL = 'CHOOSE_CHAT_MODEL';
const KEY_SLOT = 'selectionSummary.apiKey';
const API_PROTOCOL = 'https:';
const API_HOST = 'api.cometapi.com';
const ENDPOINT = `${API_PROTOCOL}//${API_HOST}/v1/chat/completions`;
const MAX_SELECTION_BYTES = 4096;
const fixture = { choices: [{
finish_reason: 'stop',
message: { role: 'assistant', content: 'The result editor is ready.' }
}] };
function completedText(payload) {
const choices = payload?.choices;
if (!Array.isArray(choices) || choices.length !== 1) return '';
const { finish_reason, message } = choices[0] ?? {};
if (finish_reason !== 'stop' || message?.role !== 'assistant' ||
message.refusal != null || typeof message.content !== 'string') return '';
const tools = message.tool_calls;
if (tools != null && (!Array.isArray(tools) || tools.length !== 0)) return '';
return message.content.trim();
}
async function requestSummary(key, selected) {
const signal = AbortSignal.timeout(30_000);
let response;
try {
response = await fetch(ENDPOINT, {
method: 'POST',
redirect: 'error',
signal,
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: MODEL,
stream: false,
messages: [{
role: 'user',
content: 'Summarize this selection in two short sentences.\n\n' + selected
}]
})
});
} catch {
return { error: signal.aborted ? 'Request timed out.' : 'Connection or redirect error.' };
}
if (!response.ok) {
await response.body?.cancel().catch(() => {});
return { error: `CometAPI returned HTTP ${response.status}.` };
}
try {
return { data: await response.json() };
} catch {
return { error: signal.aborted ? 'Timed out reading the reply.' : 'Could not read the JSON reply.' };
}
}
function activate(context) {
const { commands, window, workspace } = vscode;
const inform = (message) => { void window.showInformationMessage(message); };
const warn = (message) => { void window.showWarningMessage(message); };
let busy = false;
context.subscriptions.push(
commands.registerCommand('selectionSummary.saveKey', async () => {
try {
const input = await window.showInputBox({
prompt: 'Enter your CometAPI key', password: true, ignoreFocusOut: true
});
if (input === undefined) return;
if (!input.trim()) return warn('Enter a nonempty key.');
await context.secrets.store(KEY_SLOT, input.trim());
inform('API key saved for this extension.');
} catch { warn('Could not save the API key.'); }
}),
commands.registerCommand('selectionSummary.forgetKey', async () => {
try {
await context.secrets.delete(KEY_SLOT);
inform('API key removed from this extension.');
} catch { warn('Could not remove the API key.'); }
}),
commands.registerCommand('selectionSummary.summarize', async () => {
if (busy) return inform('A selection summary is already in progress.');
busy = true;
try {
const editor = window.activeTextEditor;
if (!editor || editor.selections.length !== 1 || editor.selection.isEmpty) {
return warn('Select one passage in a text editor.');
}
const selected = editor.document.getText(editor.selection);
const bytes = Buffer.byteLength(selected, 'utf8');
if (!selected.trim() || bytes > MAX_SELECTION_BYTES) {
return warn('Select nonblank text of at most 4096 UTF-8 bytes.');
}
let payload = fixture;
if (!FIXTURE_MODE) {
if (MODEL === 'CHOOSE_CHAT_MODEL') return warn('Set MODEL in extension.js.');
if (typeof globalThis.fetch !== 'function' ||
typeof globalThis.AbortSignal?.timeout !== 'function') {
return warn('Use a desktop extension host with fetch and AbortSignal.timeout.');
}
const key = await context.secrets.get(KEY_SLOT);
if (!key) return warn('Run CometAPI: Save API Key first.');
const action = await window.showWarningMessage(
`Send the captured selection (${bytes} bytes) to api.cometapi.com?`,
{ modal: true, detail: 'This request may incur API charges.' }, 'Send'
);
if (action !== 'Send') return;
const result = await requestSummary(key, selected);
if (result.error) return warn(result.error);
payload = result.data;
}
const text = completedText(payload);
if (!text) return warn('The reply had no completed text answer.');
const label = FIXTURE_MODE ? 'Fixture result' : 'CometAPI summary';
const document = await workspace.openTextDocument({
language: 'plaintext', content: `${label}\n\n${text}`
});
await window.showTextDocument(document, { viewColumn: vscode.ViewColumn.Beside });
} catch {
warn('Could not complete the selection summary.');
} finally {
busy = false;
}
})
);
}
module.exports = { activate };
selected is a string captured before the key lookup and confirmation. The send path uses that string throughout, even if the active editor changes. The busy flag rejects a second invocation while this one is active in the same extension instance. It neither queues another request nor coordinates across windows.
VS Code’s storage guide
documents context.secrets as encrypted storage whose entries are not synced across machines. Saving here gives the extension access when it needs to send an authorized request. Keep the value out of settings, source files, screenshots, and routine logs; see the guide to keeping keys out of tutorial repositories
.
Step 3: Run the fixture, then try one live selection
Press F5 with Run Selection Summary selected. The first extension walkthrough shows how to use the separate Extension Development Host window. In that window, make an untitled text document and paste this fixture passage:
The importer validates every row before opening a transaction.
It writes accepted rows in one batch and records skipped row numbers.
Run CometAPI: Summarize Selection without highlighting anything. Expect a selection warning and no result editor. Select the two lines and run the command again. Expect a new plain text editor beside the source with Fixture result and The result editor is ready. The fixed result verifies the local workflow; it is not a summary of those lines. Close it and confirm that the source still contains the two original sentences.
To try the live branch, consult the public model catalog reference
, then check the candidate’s model documentation for Chat Completions support. Copy the exact model ID to MODEL. A catalog entry helps you select a model; an authenticated request is still needed to check access. The model catalog validation guide
covers how to make that distinction.
- Set
FIXTURE_MODEtofalseand restart the debug session so the new host loads the change. - In the development host, run CometAPI: Save API Key and enter the key in the masked input. Saving it does not send a request.
- Highlight the same invented passage, run the summary command, and dismiss the send dialog. Expect no new result. Invoke it once more and choose Send when you intend to use the API.
- Look for a CometAPI summary tab with a short description of the validation and batch write. The wording can vary. Check whether the summary preserves the order of those steps before using it.
During preparation, the JavaScript passed a syntax check and a simulated editor and transport check on Node.js 24.22.1. Those checks covered the offline path, selection rejection, dismissal without dispatch, the original selection being sent after the input changed, and rejection of HTTP errors, malformed JSON, and truncated replies. They did not start VS Code or make an authenticated request. The desktop and live steps above are reader checks.
Failure modes
| Symptom | First useful check |
|---|---|
| The command is absent from the palette | Confirm you are in the Extension Development Host. Check the engine range and match the manifest command IDs to the registered IDs. |
| A selection warning appears | Use one contiguous, nonblank selection. Multibyte characters can hit the byte limit sooner than an equally long ASCII selection. |
| Save API Key first appears after moving to another machine | Save the key on the current machine. SecretStorage entries do not sync across machines. |
| The request reports an HTTP error | Record the numeric status and exact model ID. For an authentication error, check the key and intended account; for model or request errors, recheck the selected route and model contract. |
| The command reports no completed text answer | The response arrived but did not meet the local text policy. Review the model’s expected response form before changing the parser. |
| The request times out | The local wait ended. Treat the remote outcome as uncertain and check available usage evidence before repeating the same request. |
The extension does not surface raw HTTP error bodies. The status gives you a starting point without putting provider diagnostics or selected text in a notification. When troubleshooting the captured input in a debugger, use the invented passage and inspect the selection separately from the key or request headers.
FAQ
Does the command send the rest of the file?
The handler reads only the one selected range. It adds a fixed instruction and sends no file path or extra messages. With multiple selections it stops and asks for one passage, so it never silently picks just the first cursor’s range.
Can the reply edit the source file?
The example opens a new untitled document in plain text mode. It does not apply edits or run commands from the reply. Save or copy the result deliberately after reviewing it.
Does Forget API Key revoke the key?
It deletes this extension’s stored copy. To invalidate the credential itself, use the account controls that issued it.
Does the offline fixture validate my model?
It exercises the selection and display path with a fixed response. Model access, network connectivity, and the live response shape each need the live check with your selected model.
Reader next step
Create the three files and get the offline result tab to open first. Then select a documented Chat Completions model and run one confirmed request on the same harmless text. If you intend to use the command with your own files, review what kinds of selections may be sent and check the summary against the source before copying it into project notes.
Sources checked
The official pages were fetched November 10, 2026 UTC, November 11 in Asia/Shanghai. The maintained guides describe current interfaces; the tagged VS Code declarations match the example’s engine floor. The tutorial makes no new-release claim.
- VS Code: Commands — handler registration, manifest contributions, palette arguments, and activation.
- VS Code: Common Capabilities — encrypted extension secret storage and synchronization behavior.
- VS Code: Your First Extension — F5, the development host window, and engine compatibility.
- VS Code 1.105.0 extension API declarations — selection, input, modal, secret, and document API signatures.
- Node.js: Global objects
— Fetch and
AbortSignal.timeout. - CometAPI: Create a chat completion — route, request fields, assistant content, and finish reasons.
- CometAPI: List available CometAPI models — public model lookup, exact IDs, and endpoint hints.