Files
msd-core/vscode/browser.js
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

198 lines
8.8 KiB
JavaScript

'use strict';
/**
* MSD 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 bindMsdToVscode" — see
* the #2103 dispatch-crux note below):
*
* `host-binding.js`'s `bindMsdToVscode` 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):
* - msd-core/bin/lib/state-io.cjs → requires 'node:fs' directly.
* - msd-core/bin/lib/adapter-imperative.cjs → requires install-engine.cjs +
* capability-loader.cjs (fs/os/path).
* - msd-core/bin/lib/model-adapter.cjs → requires model-resolver.cjs →
* config-loader.cjs (fs/os/path) +
* configuration.cjs (fs/path).
* (msd-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 MSD engine
* dispatch (the msd-tools.cjs subprocess-shim `dispatchMsdCommand` used by the
* desktop `extension.js`) is fundamentally a Node `child_process.spawnSync`
* call — there is no web-worker equivalent. On web, MSD command dispatch is
* available through VS Code's NATIVE MCP client connecting to the MSD
* companion MCP server (msd-core/bin/lib/mcp-server.cjs, `msd-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 MSD 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 (
`MSD 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 msd-tools.cjs ' +
'subprocess dispatch used on desktop cannot run here). Configure the MSD MCP ' +
'server (msd-mcp-server) as a VS Code MCP server for full command dispatch on web, ' +
'or use the desktop MSD Core extension.'
);
}
/**
* Registers the `@msd` 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('msd', 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 = ['msd_progress', 'msd_workstreams', 'msd_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 `msd.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 msdCommand = vscode.commands.registerCommand('msd.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(msdCommand);
registerChatParticipant(vscode, context);
registerLanguageModelTools(vscode, context);
detectSubagentSupport(vscode);
}
module.exports = {
activate,
parseChatPrompt,
webDispatchUnavailableMessage,
registerChatParticipant,
registerLanguageModelTools,
detectSubagentSupport,
LM_TOOL_NAMES,
};