VS Code is a net-new EoS runtime that — unlike every prior migration — is NOT CLI-installed (Marketplace/VSIX extension). It has zero runtime==='vscode' branches in bin/install.js and stays that way (regression-guarded); it is driven entirely through the negotiated imperative Host-Integration adapter. Registry + validator (the hard part): - capabilities/vscode/capability.json (role:runtime): full hostIntegration block (imperative / palette / active vscode.lm model / engine hook bus / sandboxed-storage / mcp transport / sandboxed-web runtime; dispatch nested, maxDepth 5 per VS Code's documented subagent depth). - capability-validator.cjs extended so a role:runtime capability can legitimately declare "extension-distributed, no config directory": new configHome.kind:'none' + installSurface:'none' (+ GATE-A pairing + the parity maps), with localConfigDir and configHome.name made conditional on kind!=='none'. All 18 runtimes still validate; getDirName returns a distinct sentinel (not '.claude') for a no-config runtime. - The add-a-registry-runtime tax: NON_INSTALLABLE_RUNTIMES exemption in the runtime-flags drift guard, vscode added to global-config-home SPECIAL_CASED, EXPECTED_PROFILES.vscode='ide', and the config-adapter/derivation/pin-count guards updated. No golden-install fixture, model-catalog, or CONFIGURATION rows (vscode never enters allRuntimes). Dispatch + extension surface: - Fixed vscode/extension.js's createHub()-no-args bug (every dispatch was UnknownCommand, masked by a vacuous reachability test) — now reuses the shared dispatchGsdCommand subprocess-shim (Node/desktop); the reachability test is tightened to assert real dispatch. - Promoted the #1933 host binding to a shipped vscode/host-binding.js; activate() now composes the model/hookBus/stateIO seams through it. Corrected the model seam to VS Code's real API (vscode.lm.selectChatModels() -> model.sendRequest(); vscode.lm.sendRequest does not exist) so the binding actually composes on real desktop VS Code instead of throwing. - New vscode/browser.js Web Extension entry with ZERO Node APIs (the engine's config/capability loading is Node-bound, so the web entry registers the surface and directs full dispatch to the native MCP server — honestly documented). - UPGRADE 1: GSD skills as native Language Model Tools (contributes.languageModelTools + vscode.lm.registerTool), invoke() dispatching through the hub. - UPGRADE 2: native subagent dispatch wired onto #runSubagent / chat.subagents.allowInvocationsFromSubagents (fail-soft on API availability, maxDepth 5 enforced). - vscode/package.json: browser entry, engines.vscode ^1.105, chatParticipants + languageModelTools contributions; fixed a stale activationPoints->activationEvents manifest key. Added "vscode" to the package files array. Docs (## vscode matrix section) + changeset (Added). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
198 lines
8.8 KiB
JavaScript
198 lines
8.8 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* GSD extension for VS Code — WEB (browser) entry, #2103.
|
|
*
|
|
* This is the `browser` entry point (vscode/package.json `"browser": "./browser.js"`),
|
|
* loaded by VS Code Web / vscode.dev in a webworker context. It has ZERO Node
|
|
* APIs: no `require('fs')`, `require('path')`, or `require('child_process')`,
|
|
* and no Node globals (`process`, `Buffer`, `__dirname`, `__filename`). This is
|
|
* a HARD constraint, not a style preference — a Web Extension host does not
|
|
* have Node's core modules available at all; requiring one throws immediately
|
|
* at load time and breaks activation.
|
|
*
|
|
* WHY THIS FILE DOES NOT REQUIRE `./host-binding.js` OR `./extension.js`
|
|
* (a deliberate deviation from "compose the seams via bindGsdToVscode" — see
|
|
* the #2103 dispatch-crux note below):
|
|
*
|
|
* `host-binding.js`'s `bindGsdToVscode` is NOT actually web-safe once its
|
|
* transitive dependencies are checked — three of its four required engine-lib
|
|
* modules pull in Node's `fs`/`os`/`path` at module-load time (eagerly, on
|
|
* every `require()`, regardless of which code path runs):
|
|
* - gsd-core/bin/lib/state-io.cjs → requires 'node:fs' directly.
|
|
* - gsd-core/bin/lib/adapter-imperative.cjs → requires install-engine.cjs +
|
|
* capability-loader.cjs (fs/os/path).
|
|
* - gsd-core/bin/lib/model-adapter.cjs → requires model-resolver.cjs →
|
|
* config-loader.cjs (fs/os/path) +
|
|
* configuration.cjs (fs/path).
|
|
* (gsd-core/bin/lib/hook-bus.cjs alone has no requires and is genuinely
|
|
* web-safe.) Requiring `host-binding.js` here would transitively pull in
|
|
* `node:fs` and throw at web-worker load time — the opposite of "zero Node
|
|
* APIs". See host-binding.js's own header comment for the full chain. Fixing
|
|
* those engine-lib modules to be fs-free is a separate, much larger
|
|
* engine-wide refactor (config/capability loading genuinely reads files from
|
|
* disk for every OTHER host) — out of scope here; flagged rather than routed
|
|
* around silently.
|
|
*
|
|
* So this file implements its OWN minimal, independently-verified-zero-Node-API
|
|
* composition directly against `vscode.lm` — no engine-lib requires at all.
|
|
*
|
|
* DISPATCH STORY ON WEB (per the #2103 dispatch-crux design): full GSD engine
|
|
* dispatch (the gsd-tools.cjs subprocess-shim `dispatchGsdCommand` used by the
|
|
* desktop `extension.js`) is fundamentally a Node `child_process.spawnSync`
|
|
* call — there is no web-worker equivalent. On web, GSD command dispatch is
|
|
* available through VS Code's NATIVE MCP client connecting to the GSD
|
|
* companion MCP server (gsd-core/bin/lib/mcp-server.cjs, `gsd-mcp-server`
|
|
* bin entry — a separate, already-existing surface; this file does NOT
|
|
* implement an MCP client itself, it only points the user at that story).
|
|
* The chat participant and Language Model Tools registered below are
|
|
* therefore intentionally limited on web: they register (so the surface is
|
|
* discoverable and `#runSubagent`-eligible per VS Code's chat engine) but
|
|
* their handlers return an honest "configure the GSD MCP server for full
|
|
* dispatch on web" message rather than silently failing or faking success.
|
|
*/
|
|
|
|
/**
|
|
* Tokenizes a raw chat/free-form prompt string. Kept local (not shared with
|
|
* extension.js) so this file has zero requires of any kind beyond `vscode`.
|
|
* @param {string} rawPrompt
|
|
* @returns {{family: string, subcommand: string|undefined, args: string[]}}
|
|
*/
|
|
function parseChatPrompt(rawPrompt) {
|
|
const tokens = String(rawPrompt || '').trim().split(/\s+/).filter(Boolean);
|
|
return {
|
|
family: tokens[0] || '--help',
|
|
subcommand: tokens[1],
|
|
args: tokens.slice(2),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The honest "web mode" message every web-surface handler returns instead of
|
|
* attempting Node-only engine dispatch.
|
|
* @param {string} family
|
|
*/
|
|
function webDispatchUnavailableMessage(family) {
|
|
return (
|
|
`GSD web mode: full engine dispatch for "${family}" is not available in the browser ` +
|
|
'(the VS Code Web/webworker host has no Node runtime, so the gsd-tools.cjs ' +
|
|
'subprocess dispatch used on desktop cannot run here). Configure the GSD MCP ' +
|
|
'server (gsd-mcp-server) as a VS Code MCP server for full command dispatch on web, ' +
|
|
'or use the desktop GSD Core extension.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Registers the `@gsd` chat participant in web mode (#2103). Its handler is
|
|
* honest about the web dispatch limitation — see webDispatchUnavailableMessage.
|
|
* Exported separately so it is testable with a mock `vscode.chat`.
|
|
* @param {object} vscode
|
|
* @param {import('vscode').ExtensionContext} context
|
|
* @returns {object|null} the created participant, or null if vscode.chat is absent.
|
|
*/
|
|
function registerChatParticipant(vscode, context) {
|
|
if (!vscode || !vscode.chat || typeof vscode.chat.createChatParticipant !== 'function') {
|
|
return null;
|
|
}
|
|
const participant = vscode.chat.createChatParticipant('gsd', async (request, _chatContext, stream, _token) => {
|
|
const { family } = parseChatPrompt(request && request.prompt);
|
|
if (stream && typeof stream.markdown === 'function') {
|
|
stream.markdown(webDispatchUnavailableMessage(family));
|
|
}
|
|
return { metadata: { command: family, mode: 'web' } };
|
|
});
|
|
if (context && Array.isArray(context.subscriptions)) context.subscriptions.push(participant);
|
|
return participant;
|
|
}
|
|
|
|
/**
|
|
* The same representative LM tool NAMES as the desktop extension (must match
|
|
* package.json's contributes.languageModelTools[].name — underscored, the
|
|
* vscode.lm.registerTool registration name — so the manifest is identical
|
|
* across both entry points), but with web-mode invoke() handlers.
|
|
*/
|
|
const LM_TOOL_NAMES = ['gsd_progress', 'gsd_workstreams', 'gsd_plan_phase'];
|
|
|
|
/**
|
|
* Registers web-mode LM tools via vscode.lm.registerTool (#2103). Each
|
|
* invoke() returns the honest web-dispatch-unavailable message — no engine-lib
|
|
* requires, no Node APIs.
|
|
* @param {object} vscode
|
|
* @param {import('vscode').ExtensionContext} context
|
|
* @returns {number} count of tools registered (0 if vscode.lm is absent — fail-soft).
|
|
*/
|
|
function registerLanguageModelTools(vscode, context) {
|
|
if (!vscode || !vscode.lm || typeof vscode.lm.registerTool !== 'function') return 0;
|
|
let count = 0;
|
|
for (const name of LM_TOOL_NAMES) {
|
|
const impl = {
|
|
async invoke(_options, _token) {
|
|
return new vscode.LanguageModelToolResult([
|
|
new vscode.LanguageModelTextPart(webDispatchUnavailableMessage(name)),
|
|
]);
|
|
},
|
|
};
|
|
const disposable = vscode.lm.registerTool(name, impl);
|
|
if (context && Array.isArray(context.subscriptions)) context.subscriptions.push(disposable);
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Detects `#runSubagent` feature availability (`chat.subagents.allowInvocationsFromSubagents`,
|
|
* VS Code 1.105+). Fail-soft: never throws. Identical detection logic to the
|
|
* desktop extension.js (duplicated, not shared, to keep this file at zero
|
|
* requires) — see extension.js's registerSubagentDispatch for the rationale
|
|
* that VS Code's chat engine itself surfaces registered participants/tools to
|
|
* `#runSubagent`, with no separate registration API.
|
|
* @param {object} vscode
|
|
* @returns {{available: boolean}}
|
|
*/
|
|
function detectSubagentSupport(vscode) {
|
|
let available = false;
|
|
try {
|
|
const cfg = vscode && vscode.workspace && typeof vscode.workspace.getConfiguration === 'function'
|
|
? vscode.workspace.getConfiguration('chat.subagents')
|
|
: null;
|
|
available = !!(cfg && typeof cfg.get === 'function' && cfg.get('allowInvocationsFromSubagents') !== undefined);
|
|
} catch {
|
|
available = false;
|
|
}
|
|
return { available };
|
|
}
|
|
|
|
/**
|
|
* VS Code Web extension activation. Registers the chat participant + Language
|
|
* Model Tools in web mode. Does NOT register the `gsd.invoke` command with a
|
|
* real-dispatch handler (there is no Node dispatch on web) — the command is
|
|
* still contributed (contributes.commands in package.json is shared across
|
|
* desktop/web), so it is registered here too, but its handler returns the
|
|
* same honest web-mode message.
|
|
* @param {import('vscode').ExtensionContext} context
|
|
*/
|
|
function activate(context) {
|
|
const vscode = require('vscode');
|
|
|
|
const gsdCommand = vscode.commands.registerCommand('gsd.invoke', async (args) => {
|
|
const a = (args && typeof args === 'object') ? args : {};
|
|
const family = (typeof a.family === 'string' && a.family) ? a.family : '--help';
|
|
return JSON.stringify({ ok: false, stdout: '', stderr: webDispatchUnavailableMessage(family), code: null, timedOut: false });
|
|
});
|
|
context.subscriptions.push(gsdCommand);
|
|
|
|
registerChatParticipant(vscode, context);
|
|
registerLanguageModelTools(vscode, context);
|
|
detectSubagentSupport(vscode);
|
|
}
|
|
|
|
module.exports = {
|
|
activate,
|
|
parseChatPrompt,
|
|
webDispatchUnavailableMessage,
|
|
registerChatParticipant,
|
|
registerLanguageModelTools,
|
|
detectSubagentSupport,
|
|
LM_TOOL_NAMES,
|
|
};
|