diff --git a/.gitignore b/.gitignore index 1f76f168cac..c021c704752 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ test-results.xml vscode.lsif vscode.db /.profile-oss +/.vousoir-web-data +/.vousoir-dev-run /cli/target /cli/openssl product.overrides.json diff --git a/build.ps1 b/build.ps1 index 881588c0aa9..572e1228603 100644 --- a/build.ps1 +++ b/build.ps1 @@ -23,16 +23,33 @@ .PARAMETER Archive Also produce a .zip alongside the output folder. +.PARAMETER Installer + Also build an Inno Setup installer from the packaged folder. Adds a couple of + minutes on top of packaging. The installer is what registers the "Open with + Vousoir" Explorer context menu, file associations and the PATH entry; the plain + folder output registers nothing. + + To get just the context menu against an existing folder build - no installer - + use scripts\vousoir-shell-integration.ps1 instead. + +.PARAMETER InstallerTarget + user (default) installs to %LOCALAPPDATA%\Programs and needs no elevation. + system installs to Program Files and prompts for admin. + .EXAMPLE .\build.ps1 .EXAMPLE .\build.ps1 -NoMinify -Archive +.EXAMPLE + .\build.ps1 -Installer #> [CmdletBinding()] param( [ValidateSet('x64', 'arm64')][string]$Arch = 'x64', [switch]$NoMinify, - [switch]$Archive + [switch]$Archive, + [switch]$Installer, + [ValidateSet('user', 'system')][string]$InstallerTarget = 'user' ) $ErrorActionPreference = 'Stop' @@ -59,6 +76,15 @@ if (-not (Test-Path (Join-Path $repoRoot 'node_modules'))) { Fail "node_modules is missing - dependencies are not installed." "Run .\setup.ps1 first." } +# Fail before the 45-minute packaging run, not after it. +$iscc = Join-Path $repoRoot 'node_modules\innosetup\bin\ISCC.exe' +if ($Installer -and -not (Test-Path $iscc)) { + Fail "-Installer needs the Inno Setup compiler, but $iscc is missing." @" +The 'innosetup' package should have come from npm ci. Reinstall it: + npm ci +"@ +} + # Same space-free Node path the setup script establishes. Packaging shells out # to native tooling, so the node-gyp-build spaces bug can bite here too. $nodeExe = (Get-Command node).Source @@ -114,6 +140,33 @@ if (-not (Test-Path $exe)) { $sizeGb = [math]::Round((Get-ChildItem $outDir -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum / 1GB, 2) +# --- Optional installer ---------------------------------------------------- +$setupExe = $null +if ($Installer) { + # code.iss pulls tools\* out of the packaged folder without skipifsourcedoesntexist, and the + # packaging task above does not put them there - the inno-updater task does. Skipping this + # makes the Inno compile fail on a missing source file. + Write-Host "" + Write-Host "==> Staging updater tools: gulp vscode-win32-$Arch-inno-updater" -ForegroundColor Cyan + & $nodeExe $npmCli run gulp "vscode-win32-$Arch-inno-updater" + if ($LASTEXITCODE -ne 0) { Fail "Task 'vscode-win32-$Arch-inno-updater' failed." "" } + Write-Ok "tools staged" + + Write-Host "" + Write-Host "==> Building installer: gulp vscode-win32-$Arch-$InstallerTarget-setup" -ForegroundColor Cyan + Write-Warn "A few minutes. Inno Setup recompresses the whole payload." + + & $nodeExe $npmCli run gulp "vscode-win32-$Arch-$InstallerTarget-setup" + if ($LASTEXITCODE -ne 0) { Fail "Task 'vscode-win32-$Arch-$InstallerTarget-setup' failed." "" } + + # gulpfile.vscode.win32.ts writes into .build\win32-\-setup. + $setupDir = Join-Path $repoRoot ".build\win32-$Arch\$InstallerTarget-setup" + $setupExe = Get-ChildItem $setupDir -Filter *.exe -ErrorAction SilentlyContinue | + Sort-Object Length -Descending | Select-Object -First 1 + if (-not $setupExe) { Fail "The setup task reported success but no .exe is in $setupDir." "" } + Write-Ok "installer built" +} + # --- Optional archive ------------------------------------------------------ $zipPath = $null if ($Archive) { @@ -133,6 +186,17 @@ Write-Host "" Write-Host " Output $outDir ($sizeGb GB)" -ForegroundColor White if ($zipPath) { Write-Host " Archive $zipPath" -ForegroundColor White } Write-Host " Run $exe" -ForegroundColor White +if ($setupExe) { + Write-Host " Setup $($setupExe.FullName) ($([math]::Round($setupExe.Length / 1MB)) MB, $InstallerTarget install)" -ForegroundColor White + Write-Host "" + Write-Host " 'Open with Vousoir' for files and folders is checked by default in the" -ForegroundColor DarkGray + Write-Host " installer. On Windows 11 it appears under 'Show more options'." -ForegroundColor DarkGray +} else { + Write-Host "" + Write-Host " This folder registers nothing with Windows. For the Explorer context menu:" -ForegroundColor DarkGray + Write-Host " .\scripts\vousoir-shell-integration.ps1 (no installer, no elevation)" -ForegroundColor DarkGray + Write-Host " .\build.ps1 -Installer (full installer)" -ForegroundColor DarkGray +} Write-Host "" Write-Host " Note: this build is UNSIGNED. Signed installers are out of scope" -ForegroundColor DarkGray Write-Host " for v1 (work order section 10). Windows SmartScreen will warn on" -ForegroundColor DarkGray diff --git a/build/buildfile.ts b/build/buildfile.ts index c2dbccb44dd..eecdb3399a4 100644 --- a/build/buildfile.ts +++ b/build/buildfile.ts @@ -25,17 +25,12 @@ export const workbenchDesktop = [ createModuleDescription('vs/platform/files/node/watcher/watcherMain'), createModuleDescription('vs/platform/localTranscription/node/localTranscriptionMain'), createModuleDescription('vs/platform/terminal/node/ptyHostMain'), - createModuleDescription('vs/platform/agentHost/node/agentHostMain'), - createModuleDescription('vs/platform/agentHost/node/diffWorkerMain'), createModuleDescription('vs/workbench/api/node/extensionHostProcess'), - createModuleDescription('vs/workbench/workbench.desktop.main'), - createModuleDescription('vs/sessions/sessions.desktop.main') + createModuleDescription('vs/workbench/workbench.desktop.main') ]; export const workbenchWeb = createModuleDescription('vs/workbench/workbench.web.main.internal'); -export const sessionsWeb = createModuleDescription('vs/sessions/sessions.web.main.internal'); - export const keyboardMaps = [ createModuleDescription('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux'), createModuleDescription('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.darwin'), @@ -48,7 +43,6 @@ export const code = [ createModuleDescription('vs/code/node/cliProcessMain'), createModuleDescription('vs/code/electron-utility/sharedProcess/sharedProcessMain'), createModuleDescription('vs/code/electron-browser/workbench/workbench'), - createModuleDescription('vs/sessions/electron-browser/sessions'), ]; export const codeWeb = createModuleDescription('vs/code/browser/workbench/workbench'); @@ -59,8 +53,6 @@ export const codeServer = [ createModuleDescription('vs/workbench/api/node/extensionHostProcess'), createModuleDescription('vs/platform/files/node/watcher/watcherMain'), createModuleDescription('vs/platform/terminal/node/ptyHostMain'), - createModuleDescription('vs/platform/agentHost/node/agentHostMain'), - createModuleDescription('vs/platform/agentHost/node/diffWorkerMain'), ]; export const entrypoint = createModuleDescription; @@ -76,7 +68,6 @@ const buildfile = { workerBackgroundTokenization, workbenchDesktop, workbenchWeb, - sessionsWeb, keyboardMaps, code, codeWeb, diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 62cf5cf34bf..774ea4b4383 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -65,12 +65,10 @@ const vscodeResourceIncludes = [ // Workbench 'out-build/vs/code/electron-browser/workbench/workbench.html', - 'out-build/vs/sessions/electron-browser/sessions.html', // Electron Preload 'out-build/vs/base/parts/sandbox/electron-browser/preload.js', 'out-build/vs/base/parts/sandbox/electron-browser/preload-aux.js', - 'out-build/vs/platform/browserView/electron-browser/preload-browserView.js', // Node Scripts 'out-build/vs/base/node/{terminateProcess.sh,cpuUsage.sh,ps.sh}', @@ -96,13 +94,6 @@ const vscodeResourceIncludes = [ // Welcome 'out-build/vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.{svg,png}', - // Sessions - 'out-build/vs/sessions/contrib/chat/browser/media/*.svg', - 'out-build/vs/sessions/contrib/welcome/browser/media/*.svg', - 'out-build/vs/sessions/contrib/welcome/browser/media/themePreviews/*.svg', - 'out-build/vs/sessions/prompts/*.prompt.md', - 'out-build/vs/sessions/skills/**/SKILL.md', - // Extensions 'out-build/vs/workbench/contrib/extensions/browser/media/{theme-icon.png,language-icon.svg}', 'out-build/vs/workbench/services/extensionManagement/common/media/*.{svg,png}', @@ -267,11 +258,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d 'vs/workbench/workbench.desktop.main.css', 'vs/workbench/api/node/extensionHostProcess.js', 'vs/code/electron-browser/workbench/workbench.html', - 'vs/code/electron-browser/workbench/workbench.js', - 'vs/sessions/sessions.desktop.main.js', - 'vs/sessions/sessions.desktop.main.css', - 'vs/sessions/electron-browser/sessions.html', - 'vs/sessions/electron-browser/sessions.js' + 'vs/code/electron-browser/workbench/workbench.js' ]); const src = gulp.src(out + '/**', { base: '.' }) @@ -529,7 +516,13 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d result = es.merge(result, gulp.src('.build/policies/win32/**', { base: '.build/policies/win32' }) .pipe(rename(f => f.dirname = `policies/${f.dirname}`))); - if (quality === 'stable' || quality === 'insider') { + // The MSIX sparse package exists only to host the Windows 11 modern context menu, which + // needs Microsoft's signed explorer-command DLL and the CLSID that identifies it. + // Upstream gated this on quality alone because its stable/insider builds always carry + // `win32ContextMenu`; Vousoir is quality "stable" without it, so gate on the CLSID + // itself. Without this the line below dereferences undefined and packaging dies. + const win32ContextMenu = (product as { win32ContextMenu?: Record }).win32ContextMenu; + if ((quality === 'stable' || quality === 'insider') && win32ContextMenu?.[arch]) { result = es.merge(result, gulp.src('.build/win32/appx/**', { base: '.build/win32' })); const rawVersion = version.replace(/-\w+$/, '').split('.'); const appxVersion = `${rawVersion[0]}.0.${rawVersion[1]}.${rawVersion[2]}`; @@ -541,7 +534,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d .pipe(replace('@@ApplicationIdShort@@', product.win32RegValueName)) .pipe(replace('@@ApplicationExe@@', product.nameShort + '.exe')) .pipe(replace('@@FileExplorerContextMenuID@@', quality === 'stable' ? 'OpenWithCode' : 'OpenWithCodeInsiders')) - .pipe(replace('@@FileExplorerContextMenuCLSID@@', (product as { win32ContextMenu?: Record }).win32ContextMenu![arch].clsid)) + .pipe(replace('@@FileExplorerContextMenuCLSID@@', win32ContextMenu[arch].clsid)) .pipe(replace('@@FileExplorerContextMenuDLL@@', `${quality === 'stable' ? 'code' : 'code_insider'}_explorer_command_${arch}.dll`)) .pipe(rename(f => f.dirname = `appx/manifest`))); } diff --git a/build/gulpfile.vscode.web.ts b/build/gulpfile.vscode.web.ts index 17eec5de396..db0bf81ba24 100644 --- a/build/gulpfile.vscode.web.ts +++ b/build/gulpfile.vscode.web.ts @@ -112,7 +112,6 @@ const vscodeWebEntryPoints = [ buildfile.workerBackgroundTokenization, buildfile.keyboardMaps, buildfile.workbenchWeb, - buildfile.sessionsWeb, ].flat(); /** diff --git a/build/gulpfile.vscode.win32.ts b/build/gulpfile.vscode.win32.ts index 6b49e9762b2..66e7a82e84f 100644 --- a/build/gulpfile.vscode.win32.ts +++ b/build/gulpfile.vscode.win32.ts @@ -111,14 +111,17 @@ function buildWin32Setup(arch: string, target: string): task.CallbackTask { Quality: quality }; - if (quality === 'stable' || quality === 'insider') { + // Defining AppxPackageName switches code.iss onto the Windows 11 modern context menu: it + // then expects `appx\*.appx` in the packaged output and stops writing the legacy verbs. + // That package only exists when product.json declares a `win32ContextMenu` CLSID (see + // gulpfile.vscode.ts). Gating on quality alone, as upstream does, would make Inno fail on + // missing appx sources and leave a Vousoir install with no context menu at all. + const ctxMenu = (product as { win32ContextMenu?: Record }).win32ContextMenu; + if ((quality === 'stable' || quality === 'insider') && ctxMenu && ctxMenu[arch]) { definitions['AppxPackage'] = `${quality === 'stable' ? 'code' : 'code_insider'}_${arch}.appx`; definitions['AppxPackageDll'] = `${quality === 'stable' ? 'code' : 'code_insider'}_explorer_command_${arch}.dll`; definitions['AppxPackageName'] = `${product.win32AppUserModelId}`; - const ctxMenu = (product as { win32ContextMenu?: Record }).win32ContextMenu; - if (ctxMenu && ctxMenu[arch]) { - definitions['FileExplorerContextMenuCLSID'] = ctxMenu[arch].clsid; - } + definitions['FileExplorerContextMenuCLSID'] = ctxMenu[arch].clsid; } fs.writeFileSync(productJsonPath, JSON.stringify(productJson, undefined, '\t')); diff --git a/build/lib/mangle/index.ts b/build/lib/mangle/index.ts index b4f4f83a05b..e53c58d32eb 100644 --- a/build/lib/mangle/index.ts +++ b/build/lib/mangle/index.ts @@ -321,7 +321,6 @@ const skippedExportMangledFiles = [ buildfile.workerBackgroundTokenization, buildfile.workbenchDesktop, buildfile.workbenchWeb, - buildfile.sessionsWeb, buildfile.code, buildfile.codeWeb ].flat().map(x => x.name), diff --git a/build/next/index.ts b/build/next/index.ts index d791ea82543..9b1210ca1dc 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -97,13 +97,10 @@ const desktopWorkerEntryPoints = [ // Desktop workbench and code entry points const desktopEntryPoints = [ 'vs/workbench/workbench.desktop.main', - 'vs/sessions/sessions.desktop.main', 'vs/workbench/contrib/debug/node/telemetryApp', 'vs/platform/files/node/watcher/watcherMain', 'vs/platform/localTranscription/node/localTranscriptionMain', 'vs/platform/terminal/node/ptyHostMain', - 'vs/platform/agentHost/node/agentHostMain', - 'vs/platform/agentHost/node/diffWorkerMain', 'vs/workbench/api/node/extensionHostProcess', ]; @@ -111,7 +108,6 @@ const codeEntryPoints = [ 'vs/code/node/cliProcessMain', 'vs/code/electron-utility/sharedProcess/sharedProcessMain', 'vs/code/electron-browser/workbench/workbench', - 'vs/sessions/electron-browser/sessions', ]; // Web entry points (used in server-web and vscode-web) @@ -121,9 +117,7 @@ const webEntryPoints = [ ]; // Additional web-only entry points (CDN build only, not in server-web) -const webOnlyEntryPoints = [ - 'vs/sessions/sessions.web.main.internal', -]; +const webOnlyEntryPoints: string[] = []; const keyboardMapEntryPoints = [ 'vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux', @@ -136,8 +130,6 @@ const serverEntryPoints = [ 'vs/workbench/api/node/extensionHostProcess', 'vs/platform/files/node/watcher/watcherMain', 'vs/platform/terminal/node/ptyHostMain', - 'vs/platform/agentHost/node/agentHostMain', - 'vs/platform/agentHost/node/diffWorkerMain', ]; // Bootstrap files per target @@ -214,8 +206,6 @@ function getCssBundleEntryPointsForTarget(target: BuildTarget): Set { return new Set([ 'vs/workbench/workbench.desktop.main', 'vs/code/electron-browser/workbench/workbench', - 'vs/sessions/sessions.desktop.main', - 'vs/sessions/electron-browser/sessions', ]); case 'server': return new Set(); // Server has no UI @@ -227,7 +217,6 @@ function getCssBundleEntryPointsForTarget(target: BuildTarget): Set { case 'web': return new Set([ 'vs/workbench/workbench.web.main.internal', - 'vs/sessions/sessions.web.main.internal', ]); default: throw new Error(`Unknown target: ${target}`); @@ -246,9 +235,7 @@ const commonResourcePatterns = [ // SVGs referenced from CSS (needed for transpile/dev builds where CSS is copied as-is) 'vs/workbench/browser/media/code-icon.svg', - 'vs/workbench/browser/parts/editor/media/letterpress*.svg', - 'vs/sessions/contrib/chat/browser/media/*.svg', - 'vs/sessions/contrib/welcome/browser/media/themePreviews/*.svg' + 'vs/workbench/browser/parts/editor/media/letterpress*.svg' ]; // Resources for desktop target @@ -258,8 +245,6 @@ const desktopResourcePatterns = [ // HTML 'vs/code/electron-browser/workbench/workbench.html', 'vs/code/electron-browser/workbench/workbench-dev.html', - 'vs/sessions/electron-browser/sessions.html', - 'vs/sessions/electron-browser/sessions-dev.html', 'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', 'vs/workbench/contrib/webview/browser/pre/*.html', @@ -292,10 +277,6 @@ const desktopResourcePatterns = [ 'vs/workbench/services/extensionManagement/common/media/*.png', 'vs/workbench/browser/parts/editor/media/*.png', 'vs/workbench/contrib/debug/browser/media/*.png', - - // Sessions - built-in prompts and skills - 'vs/sessions/prompts/*.prompt.md', - 'vs/sessions/skills/**/SKILL.md', ]; // Resources for server target (minimal - no UI) @@ -490,7 +471,6 @@ async function copyFile(srcPath: string, destPath: string): Promise { const desktopStandaloneFiles = [ 'vs/base/parts/sandbox/electron-browser/preload.ts', 'vs/base/parts/sandbox/electron-browser/preload-aux.ts', - 'vs/platform/browserView/electron-browser/preload-browserView.ts', ]; async function compileStandaloneFiles(outDir: string, doMinify: boolean, target: BuildTarget): Promise { diff --git a/build/win32/code.iss b/build/win32/code.iss index 81220727ebe..91348b21b3f 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -9,13 +9,13 @@ AppId={#AppId} AppName={#NameLong} AppVerName={#NameVersion} AppPublisher=Firelight Innovations -AppPublisherURL=https://code.visualstudio.com/ -AppSupportURL=https://code.visualstudio.com/ -AppUpdatesURL=https://code.visualstudio.com/ +AppPublisherURL=https://github.com/vousoir/vousoir +AppSupportURL=https://github.com/vousoir/vousoir +AppUpdatesURL=https://github.com/vousoir/vousoir DefaultGroupName={#NameLong} AllowNoIcons=yes OutputDir={#OutputDir} -OutputBaseFilename=VSCodeSetup +OutputBaseFilename={#NameShort}Setup Compression=lzma SolidCompression=yes AppMutex={code:GetAppMutex} @@ -85,8 +85,10 @@ Type: files; Name: "{app}\updating_version" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 -Name: "addcontextmenufiles"; Description: "{cm:AddContextMenuFiles,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked -Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked +; Checked by default (upstream ships these unchecked). "Open with Vousoir" from Explorer is a +; primary way this build gets used, so opting out is the exception, not the default. +Name: "addcontextmenufiles"; Description: "{cm:AddContextMenuFiles,{#NameShort}}"; GroupDescription: "{cm:Other}" +Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#NameShort}}"; GroupDescription: "{cm:Other}" Name: "associatewithfiles"; Description: "{cm:AssociateWithFiles,{#NameShort}}"; GroupDescription: "{cm:Other}" Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{cm:Other}"; Check: WizardSilent @@ -1514,10 +1516,19 @@ end; function ShouldUseWindows11ContextMenu(): Boolean; begin +#ifdef AppxPackageName // Use Windows 11 context menu only if: // 1. Running on Windows 11 or later // 2. User has NOT forced Windows 10 style context menus Result := IsWindows11OrLater() and not IsWindows10ContextMenuForced(); +#else + // The modern Windows 11 menu is served by the MSIX sparse package, which this build does not + // carry (it needs Microsoft's signed explorer-command DLL and a code-signing certificate). + // Without that, claiming the modern menu would suppress the legacy verbs and leave no context + // menu at all - so always install the legacy ones. On Windows 11 they live under + // "Show more options". + Result := False; +#endif end; function HasLegacyFileContextMenu(): Boolean; diff --git a/docs/v6r/DRIVING-THE-UI.md b/docs/v6r/DRIVING-THE-UI.md new file mode 100644 index 00000000000..ecaf90fc331 --- /dev/null +++ b/docs/v6r/DRIVING-THE-UI.md @@ -0,0 +1,270 @@ +# Driving the Vousoir UI + +How to launch Vousoir and control it as an agent, so a change to the canvas or the spec panel can be +*seen* rather than inferred. Written after the first Electron launch (2026-07-27), which found three +defects that seven milestones of headless tests could not — see +[`PROGRESS.md`](./PROGRESS.md#the-first-electron-launch--2026-07-27) for what those were and why. + +**Read this first if you are changing anything visual.** Every claim below was executed on Windows +against this tree; where something is unverified it says so. + +--- + +## 1. The short version + +```powershell +# Launch. Returns immediately - Vousoir.exe is a GUI binary, PowerShell does not block on it. +.\scripts\vousoir-dev.ps1 -SkipPreLaunch +``` + +```bash +# Wait for the renderer's debug port, then attach. +curl -s http://127.0.0.1:9333/json/version # poll until this answers +npx @playwright/cli -s=ui attach --cdp=http://127.0.0.1:9333 + +# Open the demo canvas. +npx @playwright/cli -s=ui press Control+p +npx @playwright/cli -s=ui type "demo.v6r" +npx @playwright/cli -s=ui press Enter + +# Look at it. +npx @playwright/cli -s=ui screenshot --filename="C:/path/to/shot.png" +``` + +Then read the PNG. That is the whole loop. + +**Always pass `-s=`.** The CLI is backed by a daemon keyed by session name; two callers that both +omit it share one implicit session and the last `attach` wins for both. + +--- + +## 2. Launching + +`scripts/vousoir-dev.ps1` is the Windows counterpart to `.agents/skills/launch/scripts/launch.sh` +(bash-only, so it does not run here). It differs from `scripts/code.bat` in three ways that matter: + +| | Why | +|---|---| +| Isolated `--user-data-dir` / `--extensions-dir` under `.vousoir-dev-run/` | Never collides with a real Vousoir instance. Delete the directory for a clean profile. | +| `--disable-workspace-trust` | Restricted Mode is one more thing to click past on every launch. | +| `--remote-debugging-port` (default 9333) | The whole point — without it there is nothing to attach to. | + +**Flags worth knowing:** + +- `-SkipPreLaunch` skips `build/lib/preLaunch.ts`. That step downloads Electron and compiles if `out/` + is missing, and it is the slow part. Skip it when the tree is already built; drop it after a fresh + clone or if the launcher complains the binary is missing. +- `-Folder ` opens a specific workspace. **The default is a scratch copy of the demo fixture** + at `.vousoir-dev-run/demo-project`, not `vousoir/shared/src/fixtures/demo-project` itself — the + canvas writes node placements into `.vousoir/layout.json` in whatever workspace is open, and pointing + it at the committed fixture would dirty it the first time anyone drags a module. If you pass + `-Folder`, keep that in mind. +- `-Port ` for a second instance. + +**Stopping it:** + +```powershell +Get-CimInstance Win32_Process -Filter "Name='Vousoir.exe'" | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force } +``` + +Electron is 1–4 GB across ~11 processes. Kill it when you are done. + +--- + +## 3. The edit → see loop + +**Which files need what.** This is the single most useful thing on this page: + +| You changed | To see it | +|---|---| +| `extensions/vousoir-core/media/*.css`, `media/*.js` | **`Developer: Reload Window`.** Nothing else. These ship as real files loaded through `asWebviewUri`; the reload re-reads them from disk. *Verified: a CSS edit showed up after a reload with no restart.* | +| `extensions/vousoir-core/src/**/*.ts` (includes the HTML builders `canvas-html.ts`, `spec-panel-html.ts`) | `node esbuild.mts` in `extensions/vousoir-core`, **then** reload the window. These are bundled into `dist/extension.js`; editing the source alone changes nothing. | +| `vousoir/shared/**`, `typings/vousoir/**` | Rebuild the extension bundle as above — they are bundled into it too. | +| `src/vs/**` (core workbench) | Full rebuild and relaunch. Out of scope for UI/UX work on Vousoir's own surfaces. | + +Reload the window from the command palette: + +```bash +npx @playwright/cli -s=ui press Control+Shift+p +npx @playwright/cli -s=ui type "Developer: Reload Window" +npx @playwright/cli -s=ui press Enter +``` + +A reload tears down the page the CLI is attached to. **Re-attach afterwards** (a fresh `-s=` name is +the simplest way) and give it ~15s before the first screenshot; an early one times out. + +--- + +## 4. Interacting with the canvas + +The canvas and the spec panel are **webviews**, which means their content lives in a nested iframe. +That has one consequence you will hit immediately. + +### Element refs work — when the snapshot reaches into the frame + +```bash +npx @playwright/cli -s=ui snapshot +# ... - button "Add" [ref=f3e4] +npx @playwright/cli -s=ui click f3e4 +``` + +`snapshot` writes a YAML file under `.playwright-cli/` and prints its path. When it descends into the +webview you get `fe` refs for toolbar buttons and module boxes, and `click`/`dblclick`/`eval` +take them. + +**But it does not always descend.** In practice it sometimes returned only `iframe [ref=f2e2]` with +nothing under it, on the same window that had produced full refs minutes earlier. Do not build a loop +that depends on refs being there. + +### Coordinates always work + +Screenshots are taken at CSS scale, so **a pixel in the PNG is a pixel you can click**: + +```bash +npx @playwright/cli -s=ui mousemove 450 218 +npx @playwright/cli -s=ui mousedown +npx @playwright/cli -s=ui mouseup +``` + +This is the reliable path: screenshot, read the coordinate off the image, click it. + +Watch out for **nested modules**. A parent box contains its children, so the geometric centre of +"Task API" may land on "Request Validation" instead. Aim at the title row near the top of a box, not +its middle. + +### Double-click needs a real click count + +`mousedown` + `mouseup` twice does **not** produce a `dblclick` event — the CLI has no coordinate +double-click, and raw press pairs carry no click count. Drilling into a module needs `playwright-core` +directly: + +```js +// dbl.mjs - run from the repo root so `playwright-core` resolves +import { chromium } from 'playwright-core'; +const browser = await chromium.connectOverCDP('http://127.0.0.1:9333'); +const page = browser.contexts()[0].pages().find(p => p.url().includes('workbench')); +await page.mouse.click(450, 218, { clickCount: 2, delay: 40 }); +await page.waitForTimeout(2500); +await page.screenshot({ path: 'shot.png' }); +await browser.close(); +``` + +The same escape hatch covers anything else the CLI cannot express — drags with intermediate moves, +modifier-held clicks, wheel events at a position. + +### The gesture vocabulary + +What the canvas currently responds to, as of the 2026-07-27 fixes: + +| Gesture | Effect | +|---|---| +| Click a module | Selects it: outlines it, populates the spec panel, arms the toolbar's Rename/Delete | +| Click empty canvas | Clears the selection; the panel returns to its empty state | +| Double-click a module | Drills into that subtree; the toolbar title becomes `Project / Module` | +| Right-click a module | Same as click, plus a transient notice | +| Drag a module | Onto another module re-parents it; onto empty space records a manual placement in `.vousoir/layout.json` | +| Drag empty canvas | Pans | +| Wheel | Zooms toward the pointer | +| **Whole tree** button | Leaves a drilled-in view | +| **Tidy** button | Discards manual placements so auto-layout applies again | + +The spec panel is a separate webview in the sidebar, opened from the Vousoir activity-bar icon. It is +driven entirely by canvas selection — **if the canvas is broken the panel looks broken too**, which is +exactly how the 2026-07-27 bug presented. + +--- + +## 5. What is where + +The surfaces a UI/UX change will touch: + +``` +extensions/vousoir-core/ + media/canvas.css canvas styling → reload window + media/canvas.js canvas behaviour → reload window + media/spec-panel.css panel styling → reload window + media/spec-panel.js panel behaviour → reload window + src/canvas/canvas-html.ts canvas DOM skeleton, toolbar markup, CSP → rebuild + reload + src/panel/spec-panel-html.ts panel DOM skeleton → rebuild + reload +``` + +Two rules the code follows and a change should not break: + +- **Every colour is a VS Code theme variable.** No literal hex anywhere in `media/*.css` — the canvas + follows the user's theme. See [`design-tokens.instructions.md`](../../.github/instructions/design-tokens.instructions.md) + and the [`design-philosophy` skill](../../.github/skills/design-philosophy/SKILL.md) for the + vocabulary this project reasons in (values → principles → moves, not pixels). +- **No CDN, no remote font, no network.** Assets are extension files served through `asWebviewUri` + under a nonce CSP (ADR-004). Adding a `` to a font service will be blocked by the CSP, and + the CSP conformance test will fail before you see it in the app. + +--- + +## 6. Traps + +**An overlay that swallows clicks looks exactly like broken JavaScript.** This was the 2026-07-27 bug: +`#v6r-empty` set `display: flex`, which outranks the browser's `[hidden] { display: none }` on +specificity, so hiding it left an invisible full-viewport element on top of the canvas. Nothing was +visibly wrong, and nothing responded. **If a gesture does nothing, check what is actually under the +pointer before you read the handler.** `elementFromPoint` in a live webview answers this in one call; +a code review does not. + +**happy-dom has no box model.** The smoke tests +(`extensions/vousoir-core/src/canvas/canvas-webview*.smoke.test.ts`) dispatch events directly on +elements, so they bypass hit-testing entirely and cannot see overlays, stacking, overflow, or +scroll. They are worth keeping green and they are not evidence that the UI works. Only a screenshot is. + +**The CSS cascade is only partly under test.** The harness strips the stylesheet `` by default; +`withRealStyles()` in `canvas-webview-fixture.ts` injects the real `canvas.css` when a test needs +`getComputedStyle` to mean something. Use it for any fix that is fundamentally about specificity. + +**Screenshots right after a reload time out.** Give it ~15s, then retry once; the second call +usually succeeds. + +**Do not trust a green test suite as a visual sign-off.** `cd vousoir; pnpm run verify` is the gate +(279 tests, exit 0) and it must stay green — but the three defects it did not catch were all found by +looking at a screenshot. + +--- + +## 7. The browser path, and why it is not the default + +`scripts/vousoir-web.ps1` serves the workbench over HTTP for Chrome. It works up to the point of +webviews: the workbench loads, `vousoir-core` activates, the activity-bar icon renders, files open — +and then **every out-of-process iframe crashes its renderer**, so both webviews show a crashed-frame +icon. That reproduced with `https://example.com` in the same browser, so it is a browser-profile +fault, not a Vousoir one. Restarting Chrome is the first thing to try. + +Two real Vousoir gaps had to be fixed to get that far, and they are worth knowing if a web build is +ever a target: + +1. **Web webviews have no host.** Vousoir removed the Microsoft CDN fallback deliberately + (`environmentService.ts`), so `webviewExternalEndpoint` is empty in a browser and every webview + throws *"'webviewExternalEndpoint' has not been configured"*. The script writes a + `webviewContentExternalBaseUrlTemplate` into `product.overrides.json`. The `{{uuid}}` in it is + load-bearing: `pre/index.html` hashes the parent origin and checks it against its own hostname, so + webviews must be served from a wildcard subdomain, not the workbench's own origin. +2. **`quality` is missing client-side.** Running from sources the browser falls back to a product + literal with no `quality`, so it asks for `/oss-dev/vscode-remote-resource` while the server — which + reads the real `product.json` (`"quality": "stable"`) — serves `/stable-dev`. Every extension + resource 404s, including the activity-bar icon. Dev-only; a packaged build inlines the real + `product.json` into the web bundle. + +Both live in `product.overrides.json`, which is gitignored and read by `webClientServer.ts` only in dev. + +--- + +## 8. Checklist for a visual change + +1. Launch, attach, open `demo.v6r`, **screenshot the before state.** +2. Make the change. +3. Rebuild if you touched `src/`; reload the window either way. +4. Re-attach, screenshot, **compare**. +5. Exercise the gestures the change could affect — at minimum click a module, click empty canvas, and + double-click to drill in. A change to stacking or sizing can break hit-testing without changing how + anything looks. +6. `cd vousoir; pnpm run verify` — must be exit 0. +7. If you fixed a defect, add a test and **watch it fail first.** The overlay regression test was + confirmed to report `flex` instead of `none` before the fix landed; without that step it would have + been a test that proved nothing. diff --git a/docs/v6r/PRE-PUBLISH-LICENSING.md b/docs/v6r/PRE-PUBLISH-LICENSING.md new file mode 100644 index 00000000000..87b783dc511 --- /dev/null +++ b/docs/v6r/PRE-PUBLISH-LICENSING.md @@ -0,0 +1,99 @@ +# Licensing obligations to settle before publishing Vousoir + +Open items that do **not** matter while Vousoir is built and run privately, but +**do** matter the moment it is distributed to anyone else — a release binary, an +installer handed to a colleague, or a public download. + +Written 2026-07-28, when the five third-party extensions were bundled as +built-ins. Nothing here is a defect in the build; these are decisions that were +consciously deferred. + +--- + +## 1. Vendored VSIX binary — the one that needs a decision + +`vousoir/vendor/zoellner.openapi-preview-2.3.2.vsix` (929 KB) is checked into +the repository. + +**Why it is vendored:** `zoellner.openapi-preview` is not published on Open VSX +(`GET https://open-vsx.org/api/zoellner/openapi-preview` → `404`). The build's +gallery is pinned to Open VSX by `build/hygiene.ts`, so the only way to ship it +built-in was the `"vsix"` field in `product.json`, which loads a local file. + +**The problem:** the extension's license was never verified. It was obtained +from the VS Marketplace, whose Terms of Use restrict redistribution of content +obtained through it, independent of whatever license the extension itself +carries. Committing the binary to a repository that anyone else can read is +redistribution. + +**Note on repo visibility:** `Firelight-Innovations/Vousoir` was **public** at +the time this was written (`gh repo view` reported `private: false`). If the +intent is for this to stay private, that is worth checking — the vendored binary +is already reachable, and git history keeps it reachable even after a later +deletion. + +**Options, roughly in order of preference:** + +1. Confirm the upstream source repo's license permits redistribution, record it + in `ThirdPartyNotices.txt`, and keep the vendored file. +2. Drop the binary and fetch it at build time instead, so it is never + redistributed by us — the user's own machine pulls it. +3. Substitute an Open VSX equivalent (`buchenberg.scalar-openapi-preview` or + `Redocly.openapi-vs-code`), which removes the problem entirely. +4. Ship without an OpenAPI previewer. + +--- + +## 2. GPL-3.0 in the bundle + +`hediet.vscode-drawio` is **GPL-3.0**. + +It is referenced by name in `product.json` and downloaded from Open VSX at build +time, so the repository redistributes nothing. A **packaged build**, however, +contains the extension, and distributing that build triggers GPL-3.0's source +obligations for that component. + +Aggregating a GPL program alongside MIT code on the same medium is permitted — +the extension is a separate program communicating over a defined API, not a +derived work of the editor. What is required is the offer of source. Before +distributing a build: add `hediet.vscode-drawio` to `ThirdPartyNotices.txt` with +its license text and a source offer pointing at its upstream repository. + +It is also **48.9 MB** — roughly 5× the other four combined, and the dominant +size cost in the installer. Worth a second look purely on that basis. + +--- + +## 3. Third-party notices are not written + +`ThirdPartyNotices.txt` has not been updated for any of the five bundled +extensions. Licenses as recorded on Open VSX: + +| Extension | Version | License | +|---|---|---| +| `PKief.material-icon-theme` | 5.37.0 | MIT | +| `tomoki1207.pdf` | 1.2.2 | MIT | +| `hediet.vscode-drawio` | 1.6.6 | **GPL-3.0** | +| `illixion.vscode-vibrancy-continued` | 1.1.86 | MIT | +| `zoellner.openapi-preview` | 2.3.2 | **unverified** | + +The four MIT ones need attribution and license text. See §2 for draw.io and §1 +for OpenAPI Preview. + +--- + +## 4. Unrelated to licensing, but also gates a real release + +- **The build is unsigned.** `build.ps1 -Installer` currently fails on + `spawn signtool.exe ENOENT` after packaging succeeds, so no installer has ever + been produced. Distributing an unsigned installer means a SmartScreen warning + on every user's first run. +- **The Windows 11 modern context menu is out of scope** for the same reason: it + needs an MSIX sparse package signed with a real certificate. The legacy + registry menu (under "Show more options") is what ships today. +- **`illixion.vscode-vibrancy-continued` is bundled but must not be enabled.** + It patches the app's own checksum-verified files on disk, which would make + Vousoir report itself as corrupt. It is inert unless its command is invoked. + Shipping it to users who might run that command is a support problem; consider + dropping it now that vibrancy is being implemented natively (see + `vousoir/PATCHES.md` row 13). diff --git a/docs/v6r/PROGRESS.md b/docs/v6r/PROGRESS.md index 17c6e46a0fe..c3b95f015ed 100644 --- a/docs/v6r/PROGRESS.md +++ b/docs/v6r/PROGRESS.md @@ -1,12 +1,12 @@ # Vousoir (v6r) — Progress -Updated after every milestone. Docs: [`ADR.md`](./ADR.md) (why) · [`ARCHITECTURE.md`](./ARCHITECTURE.md) (how and where). +Updated after every milestone. Docs: [`ADR.md`](./ADR.md) (why) · [`ARCHITECTURE.md`](./ARCHITECTURE.md) (how and where) · [`DRIVING-THE-UI.md`](./DRIVING-THE-UI.md) (how to launch and drive the app). -**Gate for every milestone:** `cd vousoir; pnpm run verify` green. **254 tests, exit 0** — 212 at M3, plus 42 from the canvas smoke harness (PR #19). +**Gate for every milestone:** `cd vousoir; pnpm run verify` green. **279 tests, exit 0** — 212 at M3, plus 42 from the canvas smoke harness (PR #19), plus 25 since (5 of them the Electron-launch regressions below). -**All seven milestones are through, plus a canvas smoke harness.** ⚠️ **No Vousoir UI has run in -Electron yet** — the render path is exercised headlessly; see below for exactly what that does and does -not prove. That is the one outstanding item. +**All seven milestones are through, and the canvas has now run in Electron.** The outstanding item is +closed — and it was closed by finding three real defects that no headless test could see. See +[The first Electron launch](#the-first-electron-launch--2026-07-27). ## Milestones @@ -18,29 +18,52 @@ order was not milestone order — M4/M5/M6 shipped before M2/M3. |---|---|---|---|---| | **M0 — Recon** | ✅ Complete | `v6r/m0-recon` | [#11](https://github.com/Firelight-Innovations/Vousoir/pull/11) | 8 ADRs + architecture map + this tracker. Docs only. Every citation verified against the tree; 4 briefed claims were wrong. Amended throughout M1–M6 as shipped code corrected it — 13 doc defects found and fixed. | | **M1 — Model + spec store** | ✅ Complete | `v6r/m1-model` | [#12](https://github.com/Firelight-Innovations/Vousoir/pull/12) | Schema extended in place (ADR-008): typed `contracts[]`, optional given/when/then, scalar `contract` kept. `SpecStore` in `@vousoir/shared` — load/save/CRUD/nest/watch, byte-identical round trip. Brought `yaml@2.9.0` (closes D5/D7). Carried the `.v6r/` → `.vousoir/` rename into code. 22 → 66 tests. | -| **M2 — Canvas editor + auto-layout** | ⚠️ **Render path exercised headlessly — no Electron launch yet** | `v6r/m2-canvas` | [#17](https://github.com/Firelight-Innovations/Vousoir/pull/17) · [#19](https://github.com/Firelight-Innovations/Vousoir/pull/19) | `CustomTextEditorProvider` on `*.v6r`; layout engine in `@vousoir/shared` (26 tests). Manual placement + auto-tidy (ADR-003 amendment); positions in `.vousoir/layout.json`. Every structural edit routes through the M1 store. The smoke harness loads the **real** `media/canvas.js` into HTML from the **real** builder on **real** layout output, so the render path and both message directions are exercised. **Unproven: the CSP** (happy-dom does not enforce it), `asWebviewUri`, real painting, pointer semantics. 144 → 187 tests; the harness later added 42 more (212 → 254). | -| **M3 — Per-node spec panel** | ⚠️ **Render path exercised headlessly — gated on M2 for the real launch** | `v6r/m3-panel` | [#18](https://github.com/Firelight-Innovations/Vousoir/pull/18) | Sidebar webview, not inside the canvas. Spec completeness = behaviour + ≥1 contract + ≥1 test case, derived from content never `status`. External edit: dirty → warn, clean → reload. Save writes exactly one file. Smoke-covered on the same terms as M2. **Driven by canvas selection, so a blank canvas hides this too.** 187 → 212 tests. | +| **M2 — Canvas editor + auto-layout** | ✅ Complete — **verified in Electron 2026-07-27** | `v6r/m2-canvas` | [#17](https://github.com/Firelight-Innovations/Vousoir/pull/17) · [#19](https://github.com/Firelight-Innovations/Vousoir/pull/19) | `CustomTextEditorProvider` on `*.v6r`; layout engine in `@vousoir/shared` (26 tests). Manual placement + auto-tidy (ADR-003 amendment); positions in `.vousoir/layout.json`. Every structural edit routes through the M1 store. The smoke harness loads the **real** `media/canvas.js` into HTML from the **real** builder on **real** layout output, so the render path and both message directions are exercised. The four things it could not prove were settled by the Electron launch: CSP and `asWebviewUri` passed, real painting and pointer semantics turned up three defects (see below). 144 → 187 tests; the harness later added 42 more (212 → 254), the launch 5 more. | +| **M3 — Per-node spec panel** | ✅ Complete — **verified in Electron 2026-07-27** | `v6r/m3-panel` | [#18](https://github.com/Firelight-Innovations/Vousoir/pull/18) | Sidebar webview, not inside the canvas. Spec completeness = behaviour + ≥1 contract + ≥1 test case, derived from content never `status`. External edit: dirty → warn, clean → reload. Save writes exactly one file. Smoke-covered on the same terms as M2. It was indeed hidden by M2: the canvas overlay defect stopped selection ever reaching the extension host, so the panel looked broken when it was not. It populates correctly in Electron. 187 → 212 tests. | | **M4 — Work-order compiler** | ✅ Complete | `v6r/m4-compiler` | [#13](https://github.com/Firelight-Innovations/Vousoir/pull/13) | Settled scope: node's full spec + ancestors' **behaviour summaries** + neighbours' **contract blocks only, never internals**. Pure `compileWorkOrder`, separate `writeWorkOrder` to `.vousoir/cache/work-orders/`. Leak prevention is structural — the context types cannot hold a neighbour's internals. Neighbours are a structural approximation pending open question 10. 66 → 93 tests. | | **M5 — Dispatch to Claude Code** | ✅ Complete | `v6r/m5-dispatch` | [#14](https://github.com/Firelight-Innovations/Vousoir/pull/14) | Engine in `@vousoir/shared`; the extension keeps only the command. Work order to **stdin**, never argv. Transient run status, nothing writes a spec file. JSONL traces reuse `traceEventSchema`. `ELECTRON_RUN_AS_NODE` verified three ways. 93 → 112 tests. | | **M6 — Orchestration + MCP server** | ✅ Complete | `v6r/m6-mcp` | [#15](https://github.com/Firelight-Innovations/Vousoir/pull/15) | Standalone `vousoir/services/spec-mcp/`, nine tools, no cached tree, writes through the M1 store. `get_work_order` byte-identical to the editor's. Sequential orchestrator by design. Live MCP demo verified. 112 → 144 tests. | -### ⚠️ The one thing outstanding — and it is now specific +### The first Electron launch — 2026-07-27 -**No Vousoir UI has run in Electron.** The smoke harness (PR #19) closed most of this gap: it loads the -**real** `media/canvas.js` and `media/spec-panel.js` into HTML from the **real** builders on **real** -`layoutSpecTree` output, so the render path and the message protocol in both directions are exercised. +**Done, and it was worth doing.** `scripts/vousoir-dev.ps1` (new) launches the desktop app into a +throwaway profile with a CDP port; `@playwright/cli` drives it. The demo fixture opens, all four +modules render, and the spec panel populates. -**What remains, in priority order:** +**The four unknowns the harness could not reach, now settled:** -1. **The CSP — the single most likely remaining failure.** happy-dom does not enforce it, so a script - blocked by a bad nonce or a missing `localResourceRoots` entry passes headlessly and fails in - Electron. **If the canvas is blank, check this first.** -2. `asWebviewUri` resolution. -3. Real layout and painting — happy-dom has no box model, `getBoundingClientRect` is stubbed. -4. True pointer semantics. +1. **The CSP — passed.** Predicted as the most likely failure; it was not the failure. The nonce and + `localResourceRoots` are correct, `canvas.js` and `canvas.css` load in Electron. +2. **`asWebviewUri` — passed.** +3. **Real layout and painting — this is where all three defects were**, exactly the area happy-dom + cannot model. See below. +4. **True pointer semantics — passed** once the defects were fixed. -**One human session settles all four:** launch `scripts/code.bat`, open a `*.v6r` file, confirm nodes -render, select one, confirm the panel populates. M3 is gated on M2 here — a blank canvas hides both. +**Three defects, all invisible to a DOM without a box model:** + +| Defect | Why no test caught it | +|---|---| +| **`#v6r-empty` swallowed every pointer event.** Its `display: flex` outranks the UA's `[hidden] { display: none }` on specificity, so hiding it left an invisible full-viewport element over the canvas. **This was the whole reported bug** — no click, drag, zoom or double-click reached a module, and because selection never crossed to the extension host, the spec panel stayed empty too. Fixed by restating the rule at ID specificity. | The smoke tests dispatch events directly on the node element, so they bypass hit-testing entirely. happy-dom has no box model and no `elementFromPoint` worth trusting. | +| **Selection was split across two gestures.** Left-click posted `selectNode` (drove the panel); right-click set the webview's local `selectedId` (drove the toolbar). Neither did both, so whichever gesture you used, the other surface disagreed — and the hint text told you to right-click, the one that never reached the panel. Both now route through one `select()`. | Each half was tested, separately, and each half passed. Nothing asserted they were the *same* selection. | +| **The drilled-in title was computed and thrown away.** `#render` sends `projectName: "Project / Module"`; `canvas.js` never read it, so after drilling in nothing said which subtree you were looking at. | The protocol carried the field and the provider set it correctly. No test asserted the webview *applied* it. | + +A selection outline (`.v6r-selected`) was added at the same time — before it, a selected module looked +identical to an unselected one. + +**Five regression tests** cover all three, plus the CSS cascade the harness previously skipped +(`withRealStyles()` injects the real `canvas.css` so `getComputedStyle` is meaningful). The overlay +test was watched to fail — `flex`, not `none` — before the fix landed. + +**The browser path is set up but blocked on the environment.** `scripts/vousoir-web.ps1` serves the +workbench for Chrome, and needed two fixes to get that far: web webviews have no host (Vousoir removed +the CDN fallback by design, so `product.overrides.json` must supply a +`webviewContentExternalBaseUrlTemplate`), and running from sources the browser's product literal has no +`quality`, so it requests `/oss-dev/…` while the server serves `/stable-dev/…` and every extension +resource 404s. Both are dev-only; a packaged build inlines the real `product.json`. With those fixed +the workbench loads, the extension activates and the activity-bar icon appears — but **every +out-of-process iframe crashes its renderer in the Chrome profile tested**, including +`https://example.com`, so webviews cannot be exercised there. That is a browser fault, not a Vousoir +one. Desktop is the working automation path. ## Decision log @@ -54,6 +77,9 @@ render, select one, confirm the panel populates. M3 is gated on M2 here — a bl | 2026-07-24 | MCP server is a standalone stdio package, not an extension of the service-host protocol; nine merged tools | [ADR-006](./ADR.md) | | 2026-07-24 | Develop in a git worktree with junctioned dependencies — accepted as time-boxed debt | [ADR-007](./ADR.md) | | 2026-07-24 | Extend `specNodeFrontmatterSchema` in place; never fork it into a parallel `ModuleNode` | [ADR-008](./ADR.md) | +| 2026-07-27 | **One gesture selects for both surfaces.** Click sets the webview's `selectedId` *and* posts `selectNode`; right-click does the same and adds a notice. Two gestures each driving one half of the selection is a state split, and the two halves will disagree | `media/canvas.js` `select()` | +| 2026-07-27 | **Any element that overlays the canvas restates `[hidden]` at its own specificity.** An ID rule with `display` silently outranks the UA's `[hidden] { display: none }`, and the failure mode — invisible, still hit-testable — looks like broken JavaScript, not broken CSS | `media/canvas.css` | +| 2026-07-27 | **Desktop, not browser, is Vousoir's UI-automation path.** `scripts/vousoir-dev.ps1` + `@playwright/cli` over CDP. The browser route works up to the point of webviews and is kept (`scripts/vousoir-web.ps1`), but it depends on out-of-process iframes, which are a per-browser-profile risk | `PATCHES.md` row 9 | ### User rulings on PR #11 — 2026-07-24 diff --git a/extensions/vousoir-core/media/canvas.css b/extensions/vousoir-core/media/canvas.css index db247278ca7..f42dee08f88 100644 --- a/extensions/vousoir-core/media/canvas.css +++ b/extensions/vousoir-core/media/canvas.css @@ -116,6 +116,12 @@ body { border-style: dashed; } +/* Outline rather than a border swap: a border change would reflow the 1px and shift the label. */ +.v6r-node.v6r-selected { + outline: 2px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + .v6r-node-title { display: block; padding: 8px 10px 0; @@ -143,3 +149,14 @@ body { white-space: pre-wrap; opacity: 0.8; } + +/* + * `display: flex` above beats the user-agent's `[hidden] { display: none }` on specificity, + * so `empty.hidden = true` would leave this sitting over the whole viewport - invisible, + * because it is empty, but swallowing every click, drag and wheel aimed at the canvas. + * Restate the rule at ID specificity. Same for the notice, which is painted over the canvas. + */ +#v6r-empty[hidden], +#v6r-notice[hidden] { + display: none; +} diff --git a/extensions/vousoir-core/media/canvas.js b/extensions/vousoir-core/media/canvas.js index 171cb82b0ed..13d10b84f2b 100644 --- a/extensions/vousoir-core/media/canvas.js +++ b/extensions/vousoir-core/media/canvas.js @@ -20,6 +20,7 @@ const viewport = document.getElementById('v6r-viewport'); const surface = document.getElementById('v6r-surface'); const empty = document.getElementById('v6r-empty'); + const project = document.getElementById('v6r-project'); const notice = document.getElementById('v6r-notice'); let noticeTimer = null; @@ -37,6 +38,10 @@ } function render(message) { + // Drilling in re-titles the canvas "Project / Module" (v6r-canvas-provider #render), so + // this is the only thing telling the user which subtree they are looking at. + project.textContent = message.projectName; + surface.replaceChildren(); surface.style.width = `${message.width}px`; surface.style.height = `${message.height}px`; @@ -54,6 +59,28 @@ for (const box of [...message.boxes].sort((a, b) => a.depth - b.depth)) { surface.append(renderBox(box)); } + // A render replaces every element, so the selection outline has to be reapplied. The + // selected module may also have been deleted underneath us, in which case nothing matches. + markSelected(); + } + + /** Paints the selection outline on whichever node is currently selected, if any. */ + function markSelected() { + for (const element of surface.children) { + element.classList.toggle('v6r-selected', selectedId !== null && element.dataset.id === selectedId); + } + } + + /** + * One gesture selects for both surfaces: `selectedId` drives the toolbar here in the + * webview, and the posted message drives the spec panel across the extension host. They + * were previously set by different gestures - left-click told the panel, right-click told + * the toolbar - so whichever one you used, the other disagreed. + */ + function select(id) { + selectedId = id; + markSelected(); + vscode.postMessage({ type: 'selectNode', id: id }); } function renderBox(box) { @@ -79,7 +106,7 @@ element.addEventListener('mousedown', (event) => { // Stop the pan handler claiming a gesture that was aimed at a node. event.stopPropagation(); - vscode.postMessage({ type: 'selectNode', id: box.id }); + select(box.id); dragging = { id: box.id, box: box, @@ -96,7 +123,7 @@ element.addEventListener('contextmenu', (event) => { event.preventDefault(); event.stopPropagation(); - selectedId = box.id; + select(box.id); showNotice('Selected "' + box.title + '". Use the toolbar to add, rename or delete.'); }); return element; @@ -169,7 +196,7 @@ } panning = { x: event.clientX - view.x, y: event.clientY - view.y }; viewport.classList.add('v6r-panning'); - vscode.postMessage({ type: 'selectNode', id: null }); + select(null); }); window.addEventListener('mousemove', (event) => { @@ -259,11 +286,11 @@ vscode.postMessage({ type: 'createNode', parent: selectedId }); }); document.getElementById('v6r-rename').addEventListener('click', () => { - if (selectedId === null) { showNotice('Select a module first (right-click it).'); return; } + if (selectedId === null) { showNotice('Select a module first by clicking it.'); return; } vscode.postMessage({ type: 'renameNode', id: selectedId }); }); document.getElementById('v6r-delete').addEventListener('click', () => { - if (selectedId === null) { showNotice('Select a module first (right-click it).'); return; } + if (selectedId === null) { showNotice('Select a module first by clicking it.'); return; } vscode.postMessage({ type: 'deleteNode', id: selectedId }); }); document.getElementById('v6r-tidy').addEventListener('click', () => { diff --git a/extensions/vousoir-core/package.json b/extensions/vousoir-core/package.json index 7df1200ee66..26c2cecdcba 100644 --- a/extensions/vousoir-core/package.json +++ b/extensions/vousoir-core/package.json @@ -67,7 +67,10 @@ } ] } - ] + ], + "configurationDefaults": { + "workbench.iconTheme": "material-icon-theme" + } }, "scripts": { "compile": "node esbuild.mts", diff --git a/extensions/vousoir-core/src/canvas/canvas-html.ts b/extensions/vousoir-core/src/canvas/canvas-html.ts index 691a9075f24..d7ccddaaf13 100644 --- a/extensions/vousoir-core/src/canvas/canvas-html.ts +++ b/extensions/vousoir-core/src/canvas/canvas-html.ts @@ -44,7 +44,7 @@ export function canvasHtml(webview: Webview, mediaRoot: Uri, projectName: string - right-click a module to select · double-click to drill in · drag to pan · scroll to zoom + click a module to select · double-click to drill in · drag to pan · scroll to zoom
diff --git a/extensions/vousoir-core/src/canvas/canvas-webview-fixture.ts b/extensions/vousoir-core/src/canvas/canvas-webview-fixture.ts new file mode 100644 index 00000000000..08fd4d2d7f6 --- /dev/null +++ b/extensions/vousoir-core/src/canvas/canvas-webview-fixture.ts @@ -0,0 +1,70 @@ +/** + * Test-only fixture shared by the canvas smoke tests. + * + * The render/interaction tests and the pointer-gesture tests mount the same webview from the + * same tree; this is that setup, extracted so neither file has to restate it and neither + * grows past the line cap. It builds nothing the tests cannot see — every helper is a thin + * wrapper over the real `canvasHtml` builder, the real layout, and the real `media/canvas.js`. + */ + +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { buildSpecTree, layoutSpecTree } from '@vousoir/shared'; +import type { CanvasBox, SpecNode, SpecNodeFrontmatter, SpecTree } from '@vousoir/typings'; +import { canvasHtml } from './canvas-html.ts'; +import { MEDIA_DIR, fakeUri, fakeWebview, mountWebview, type MountedWebview } from '../webview-harness.ts'; + +export function node(id: string, parent: string | null, title = id): SpecNode { + const frontmatter: SpecNodeFrontmatter = { id, title, parent, status: 'specified' }; + return { id, filePath: `/repo/.vousoir/spec/${id}.md`, frontmatter, body: '' }; +} + +/** The DoD shape: three modules with one nested. */ +export function demoTree(): SpecTree { + return buildSpecTree([ + node('root', null, 'Vousoir Demo'), + node('api', 'root', 'Task API'), + node('validation', 'api', 'Request Validation'), + node('storage', 'root', 'Task Store'), + ]); +} + +export function renderMessage(tree: SpecTree = demoTree()): Record { + const layout = layoutSpecTree(tree); + return { + type: 'render', + projectName: 'Vousoir Demo', + width: layout.width, + height: layout.height, + boxes: layout.boxes.map((box: CanvasBox) => ({ ...box })), + }; +} + +export function nodes(): Element[] { + return [...document.querySelectorAll('.v6r-node')]; +} + +export function nodeFor(id: string): HTMLElement { + const element = document.querySelector(`.v6r-node[data-id="${id}"]`); + if (element === null) { + throw new Error(`no rendered node for "${id}"`); + } + return element as HTMLElement; +} + +/** Mounts the shipped `canvas.js` into HTML from the shipped builder. */ +export function mountCanvas(): MountedWebview { + const html = canvasHtml(fakeWebview() as never, fakeUri('/media') as never, 'Vousoir Demo'); + return mountWebview(html, 'canvas.js'); +} + +/** + * The harness strips the stylesheet `` (see its caveats). A test that needs the real + * cascade injects `canvas.css` as a ` \ No newline at end of file + + + + diff --git a/src/vs/workbench/electron-browser/desktop.contribution.ts b/src/vs/workbench/electron-browser/desktop.contribution.ts index 117b6994794..70f2172a1ed 100644 --- a/src/vs/workbench/electron-browser/desktop.contribution.ts +++ b/src/vs/workbench/electron-browser/desktop.contribution.ts @@ -333,6 +333,27 @@ import product from '../../platform/product/common/product.js'; return windowBorderDescription; })(), 'included': isWindows + }, + 'window.vibrancy': { + 'type': 'string', + 'enum': ['none', 'mica', 'acrylic', 'tabbed'], + 'default': 'none', + 'scope': ConfigurationScope.APPLICATION, + 'markdownEnumDescriptions': [ + localize('window.vibrancy.none', "Draw an opaque window background."), + localize('window.vibrancy.mica', "Tint the window with the desktop wallpaper."), + localize('window.vibrancy.acrylic', "Blur whatever is behind the window."), + localize('window.vibrancy.tabbed', "Tint the window with the desktop wallpaper, using the variant intended for tabbed windows."), + ], + 'markdownDescription': localize('window.vibrancy', "Render the workbench over a translucent, system-drawn window background. Requires Windows 11 22H2 or newer, or macOS, and has no effect elsewhere. On macOS all values other than {0} map to the closest system material. Changes require a full restart to apply.", '`none`'), + // Deliberately withheld. The implementation compiles and runs, but the workbench + // surfaces in `vibrancy.ts` do not yet cover every part: empty pane areas (the space + // below the last file in the explorer, for one) paint nothing and let the system + // backdrop through, which reads as a washed-out panel rather than a translucent one. + // Not registering the setting means `getWindowVibrancy` always resolves to `none`, + // so no window is ever created with a backdrop. Restore `isWindows || isMacintosh` + // to resume the work. + 'included': false } } }); diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 5b8eb7755e0..b5d58edabad 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -61,6 +61,7 @@ import { ElectronRemoteResourceLoader } from '../../platform/remote/electron-bro import { IConfigurationService } from '../../platform/configuration/common/configuration.js'; import { applyZoom } from '../../platform/window/electron-browser/window.js'; import { mainWindow } from '../../base/browser/window.js'; +import { VIBRANCY_CLASS_NAME } from './vibrancy.js'; export class DesktopMain extends Disposable { @@ -150,11 +151,17 @@ export class DesktopMain extends Disposable { } private getExtraClasses(): string[] { + const classes: string[] = []; + if (isMacintosh && isTahoeOrNewer(this.configuration.os.release)) { - return ['macos-tahoe']; + classes.push('macos-tahoe'); + } + + if (this.configuration.vibrancy) { + classes.push(VIBRANCY_CLASS_NAME); } - return []; + return classes; } private registerListeners(workbench: Workbench, storageService: NativeWorkbenchStorageService): void { diff --git a/src/vs/workbench/electron-browser/vibrancy.ts b/src/vs/workbench/electron-browser/vibrancy.ts new file mode 100644 index 00000000000..20a17f41aef --- /dev/null +++ b/src/vs/workbench/electron-browser/vibrancy.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerThemingParticipant } from '../../platform/theme/common/themeService.js'; +import { editorBackground } from '../../platform/theme/common/colorRegistry.js'; +import { ColorIdentifier } from '../../platform/theme/common/colorUtils.js'; +import { ACTIVITY_BAR_BACKGROUND, EDITOR_GROUP_HEADER_TABS_BACKGROUND, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, STATUS_BAR_BACKGROUND, TITLE_BAR_ACTIVE_BACKGROUND } from '../common/theme.js'; + +/** + * Marks a workbench that lives in a window with a system-drawn backdrop + * (`window.vibrancy`). Only the flat surfaces that make up the window + * background become translucent; everything drawn on top of them keeps its + * theme color so that text and controls stay legible. + */ +export const VIBRANCY_CLASS_NAME = 'vibrancy'; + +/** + * How much of a surface's own theme color survives. The remainder is what the + * system backdrop shows through. + */ +const SURFACE_ALPHA = 0.6; + +registerThemingParticipant((theme, collector) => { + const root = `.monaco-workbench.${VIBRANCY_CLASS_NAME}`; + + // The workbench root itself is fully transparent. The parts below each paint + // their own translucent surface over it, and stacking two translucent layers + // would make the window noticeably more opaque than intended. + collector.addRule(`${root} { background-color: transparent; }`); + + // Parts assign their background as an inline style (see the `updateStyles` + // of each part), so these rules have to be `!important` to take effect. + const addSurfaceRule = (selector: string, colorId: ColorIdentifier) => { + const color = theme.getColor(colorId); + if (color) { + collector.addRule(`${selector} { background-color: ${color.transparent(SURFACE_ALPHA)} !important; }`); + } + }; + + addSurfaceRule(`${root} .part.titlebar`, TITLE_BAR_ACTIVE_BACKGROUND); + addSurfaceRule(`${root} .part.activitybar`, ACTIVITY_BAR_BACKGROUND); + addSurfaceRule(`${root} .part.sidebar, ${root} .part.auxiliarybar`, SIDE_BAR_BACKGROUND); + addSurfaceRule(`${root} .part.panel`, PANEL_BACKGROUND); + addSurfaceRule(`${root} .part.statusbar`, STATUS_BAR_BACKGROUND); + addSurfaceRule(`${root} .editor-group-container > .title`, EDITOR_GROUP_HEADER_TABS_BACKGROUND); + addSurfaceRule(`${root} .editor-group-container > .editor-container`, editorBackground); + + // The editor paints `editor.background` again on top of its container, which + // would put an opaque layer straight back over the translucent one. + collector.addRule(` + ${root} .editor-group-container > .editor-container .monaco-editor, + ${root} .editor-group-container > .editor-container .monaco-editor-background { + background-color: transparent; + } + `); +}); diff --git a/vousoir/PATCHES.md b/vousoir/PATCHES.md index e4ea7897a25..cb886f5ec3a 100644 --- a/vousoir/PATCHES.md +++ b/vousoir/PATCHES.md @@ -65,6 +65,16 @@ facts below are unchanged by the excision.) | 6 | `build/hygiene.ts` | Replaced the blanket *"product.json: Contains 'extensionsGallery'"* error with a check that the gallery **is** Open VSX. Upstream forbids any gallery because the Microsoft Marketplace is not licensed for the OSS build; §4.3 *requires* Vousoir to ship one. Rather than delete the guard, it now fails if `extensionsGallery.serviceUrl` is anything other than `https://open-vsx.org/…` — so an accidental repoint at the Microsoft Marketplace still breaks hygiene, which is the risk the original check existed to prevent | The upstream rule and this fork's requirements are in direct conflict; hygiene fails on every commit otherwise. Inverting the check preserves its intent instead of discarding it | Medium. Small, self-contained block. If upstream rewrites this function the conflict is obvious and the resolution is to re-apply the inverted check. | | 7 | `build/filters.ts` | Added `!extensions/vousoir-*/**`, `!typings/vousoir/**`, `!vousoir/**` to `copyrightFilter` | Hygiene requires the *Microsoft* copyright header on every source file. Asserting Microsoft's copyright over code they did not write is false attribution, so first-party Vousoir code is exempt. The **unicode and indentation** filters were deliberately *not* touched — those are reasonable conventions, and our files were fixed to comply instead | Low. Three appended lines in a long exclusion list; conflicts resolve by keeping both sides. | | 8 | `eslint.config.js` | Appended a flat-config override setting `header/header: 'off'` for `extensions/vousoir-*/**`, `typings/vousoir/**`, `vousoir/**` | Same reason as #7 — the ESLint half of the same rule. Implemented as a trailing override rather than editing upstream's rule body: flat config is last-match-wins, so `src/` and every upstream extension still require the Microsoft header | Low. Purely additive block at the end of the array; nothing upstream is modified. | +| 9 | `scripts/vousoir-dev.ps1`, `scripts/vousoir-web.ps1` (new); `.gitignore` (3 appended lines) | Two dev launchers. `vousoir-dev.ps1` starts the desktop app in a throwaway profile with a CDP port, so the workbench can be driven by `@playwright/cli` — the Windows counterpart to the bash-only `.agents/skills/launch/scripts/launch.sh`. `vousoir-web.ps1` starts the server for a browser, and writes the `product.overrides.json` the browser workbench needs: a `webviewContentExternalBaseUrlTemplate` (Vousoir removed the CDN fallback by design, so web webviews have no host otherwise) and a `quality` (running from sources the browser falls back to a product literal with no `quality`, so it requests `/oss-dev/…` while the server serves `/stable-dev/…` and every extension resource 404s). `.gitignore` gained the two run directories; `product.overrides.json` was already ignored | No script existed to run Vousoir anywhere a UI agent could reach it, which is why no Vousoir UI had been exercised in a real browser engine through seven milestones. Both are additive files under `scripts/`; neither changes what ships. The `product.overrides.json` values are **dev-only** — `webClientServer.ts` reads that file only when `!isBuilt`, and a packaged build inlines the real `product.json` into the web bundle, so the `quality` mismatch cannot occur there | Low. New files upstream does not have, plus an append to `.gitignore`. | +| 10 | `product.json` | Appended **5 entries** to `builtInExtensions` so a packaged build ships them present and enabled with no marketplace install step: `PKief.material-icon-theme@5.37.0`, `tomoki1207.pdf@1.2.2`, `hediet.vscode-drawio@1.6.6` and `illixion.vscode-vibrancy-continued@1.1.86` (all four fetched from the configured Open VSX gallery via `fromMarketplace()`), plus `zoellner.openapi-preview@2.3.2`, which is **not published on Open VSX** and is therefore vendored as a local VSIX through the optional `"vsix"` field (`fromVsix()`). Every `metadata.id` / `publisherId.publisherId` UUID was read from a live gallery `extensionquery` response rather than invented; every `sha256` was computed over the exact bytes `fetchUrl()` hashes, and proven by wiping `.build/builtInExtensions` and forcing a full re-download | These five are the intended Vousoir out-of-the-box surface. `builtInExtensions` is the sanctioned data-only mechanism — no source logic touched, same shape upstream already uses for its three `ms-vscode.*` entries | **Low.** Purely additive array entries in a leaf config file; a conflict could only arise if upstream edits its own three, resolved by keeping both sides. The standing cost is ours: `builtInExtensionsEnabledWithAutoUpdates` is `[]`, so pinned version **and** checksum must be bumped together by hand or the build breaks. | +| 11 | `extensions/vousoir-core/package.json`; `vousoir/vendor/zoellner.openapi-preview-2.3.2.vsix` (new, 929,364 bytes) | Added `contributes.configurationDefaults: { "workbench.iconTheme": "material-icon-theme" }`, making Material Icon Theme the **default** icon theme rather than merely present. Verified against this tree, not assumed: `configurationExtensionPoint.ts:177` registers the ext point and its handler rejects an override only when the setting declares `disallowConfigurationDefault` or sits outside the allowed scopes; `workbench.iconTheme` (`themeConfiguration.ts:102`) declares neither, so it takes the registry default `WINDOW`, which is on the allow-list. Also checked in the vendored VSIX that patch 10 references | Achieves the default **without a core patch**. The alternative was editing `ThemeSettingDefaults.FILE_ICON_THEME` in `workbenchThemeService.ts`, which was deliberately not done. A user's explicit setting still wins — extension defaults sit below user settings | **None** upstream — both are first-party additive files. The VSIX will go stale; re-vendoring means re-downloading from the Marketplace `/vspackage` URL and re-pinning the sha. | +| 12 | `src/vs/workbench/browser/media/code-icon.svg` | Replaced Microsoft's blue document mark with the Vousoir wedge on a dark rounded tile (`#20232A` tile, `#F5F5F0` wedge, sampled from the shipped `resources/win32/code_150x150.png` so the in-app mark and the desktop icon agree). The wedge path is the same `M9 2H15L18.5 22H5.5L9 2Z` used everywhere else, copied verbatim rather than redrawn. Deliberately a self-contained tile, not the bare `currentColor` outline of `extensions/vousoir-core/media/vousoir-icon.svg`: this file is consumed as a CSS `background-image`, where `currentColor` cannot inherit, and an outline-only wedge would vanish at the titlebar's 16px and be invisible on a light titlebar | Completes patch #2 (app-identity artwork), which covered `resources/` but missed this one under `src/`. One file fixes five call sites — titlebar, banner, update tooltip, getting-started and walkthrough — and the filename is unchanged, so `build/next/index.ts`'s asset list needed no edit | Low. Upstream only touches these bytes when it redesigns its own icon; resolution is "keep ours" every time. | +| 13 | `src/vs/platform/window/common/window.ts`, `src/vs/platform/windows/electron-main/windows.ts`, `windowsMainService.ts`, `src/vs/platform/theme/electron-main/themeMainServiceImpl.ts`, `src/vs/code/electron-browser/workbench/workbench.ts`, `src/vs/workbench/electron-browser/desktop.{contribution,main}.ts`, `src/vs/workbench/electron-browser/vibrancy.ts` (new) | Native window vibrancy: a `window.vibrancy` setting (`none`/`mica`/`acrylic`/`tabbed`) mapped to Electron's `backgroundMaterial` on Windows 11 22H2+ and `vibrancy` on macOS, with a workbench-side theming participant that makes the parts translucent. **Currently withheld** — `'included': false` on the config, so the setting is never registered, `getWindowVibrancy` always resolves to `none`, and no window can be created with a backdrop. The code compiles and `typecheck-client` is green | Implemented natively instead of shipping `illixion.vscode-vibrancy-continued`'s approach, which patches VS Code's checksum-verified HTML/JS on disk, triggers the "installation appears corrupt" warning against our own product, and needs write access to the install directory. Withheld because `vibrancy.ts` does not yet cover every surface: empty pane areas (the space below the last file in the explorer) paint nothing and let the backdrop through, reading as a washed-out panel. Restore `isWindows \|\| isMacintosh` to resume | Medium. Touches six core files, though each edit is small and additive. `defaultBrowserWindowOptions` and `getExtraClasses` are the likeliest conflict sites. | +| 14 | `scripts/vousoir-shell-integration.ps1` (new) | Standalone registrar for the "Open with Vousoir" Explorer context menu against an unpackaged `Vousoir.exe`, so shell integration no longer requires building an installer. Writes five keys under `HKCU\Software\Classes` — `*\shell\Vousoir` (files, `"%1"`), `Directory\shell\Vousoir`, `Directory\Background\shell\Vousoir` (the empty space inside an open folder), `Drive\shell\Vousoir` (all `"%V"`) and `DesktopBackground\Shell\Vousoir`. Key name, `Icon`, `expandsz` value types and the `Open w&ith &Vousoir` label are read from `product.json` and reproduce `code.iss` lines 1275-1290 exactly, so script and installer cannot drift. `DesktopBackground` is the one addition beyond code.iss: Explorer routes desktop right-click to a separate class and does not substitute `%V`, so the path is resolved via `SpecialFolder.DesktopDirectory` at registration time (this machine's Desktop is OneDrive-redirected, which `%USERPROFILE%\Desktop` would have missed). Uses the .NET `Microsoft.Win32.Registry` API rather than the PowerShell provider, for which `*\shell\...` is a wildcard. `-Uninstall` deletes exactly those five keys and prunes parents left with zero subkeys and zero values, stopping at `Software\Classes` | `build.ps1` produced a runnable folder, not an installer, so none of code.iss's shell integration ever landed. HKCU-only means no elevation and no machine-wide surface | Low. New file upstream does not have. | +| 15 | `build/gulpfile.vscode.ts` (win32 appx block), `build/gulpfile.vscode.win32.ts` (`buildWin32Setup`) | Gated the MSIX/appx definitions on `product.win32ContextMenu?.[arch]` in addition to `quality`. Upstream gates on `quality` alone because its stable/insider builds always carry a `win32ContextMenu` CLSID. Vousoir is `quality: "stable"` **without** that key, which broke two things: (a) `gulpfile.vscode.ts` dereferenced `product.win32ContextMenu![arch].clsid` on `undefined`, so `gulp vscode-win32-x64-min` — the whole of `build.ps1` — threw while building the packaging stream; (b) `gulpfile.vscode.win32.ts` defined `AppxPackageName`, which switches `code.iss` onto the Windows 11 modern menu and makes it require `appx\code_x64.appx`, a file only Microsoft's CI produces. Verified empirically: ISCC on a stub tree fails with `Source file "…\appx\code_x64.appx" does not exist` when the appx definitions are passed, and compiles cleanly when they are not | **This is why no packaged build had ever succeeded on this fork.** The appx exists only to host the Windows 11 modern menu, which needs Microsoft's signed `code_explorer_command_x64.dll` and a code-signing certificate; Vousoir has neither, so the correct gate is "does product.json declare the CLSID", not "what is the quality string" | Low-medium. Both are small conditionals in build-only files; a conflict appears as an upstream rewrite of the same `if`, resolved by keeping the extra `win32ContextMenu` term. Reverts itself naturally if `win32ContextMenu` is ever added. | +| 16 | `build/win32/code.iss` | Four changes. (1) `addcontextmenufiles` / `addcontextmenufolders` lost `Flags: unchecked`, so "Open with Vousoir" is checked by default. (2) `ShouldUseWindows11ContextMenu()` returns `False` unless `AppxPackageName` is defined — without this, (1) would have had no effect on Windows 11: the legacy `*\shell` verbs carry `Check: not ShouldUseWindows11ContextMenu` and the folder verbs go through `ShouldInstallLegacyFolderContextMenu()`, which ands in the same term, so the installer would claim the modern menu, write a lone `VousoirContextMenu\Title` marker for an appx that does not exist, and leave **no context menu at all**. (3) `AppPublisherURL` / `AppSupportURL` / `AppUpdatesURL` moved off `code.visualstudio.com` to `github.com/vousoir/vousoir`. (4) `OutputBaseFilename=VSCodeSetup` → `{#NameShort}Setup`, so the artifact is `VousoirSetup.exe`. Whole-file ISCC compile verified green | (1) and (2) are the owner's request, and (2) is what makes (1) real on Windows 11. (3) and (4) are §9.2 — both are user-visible, (3) in Add/Remove Programs | Medium. `code.iss` is a large upstream file under active change; the `#ifdef AppxPackageName` guard sits inside a function upstream also edits, so a conflict is a re-apply of the same 5-line wrapper. Only the dead `build/azure-pipelines/win32/**` referenced `VSCodeSetup.exe`. | +| 17 | `build.ps1` | Added `-Installer` and `-InstallerTarget ` (default `user`, no elevation). Runs `gulp vscode-win32--inno-updater` then `gulp vscode-win32---setup` after packaging, and reports where the setup .exe landed. The inno-updater step is not optional: `code.iss` has `Source: "tools\*"` with no `skipifsourcedoesntexist`, and `vscode-win32-x64-min` does not populate that folder. ISCC presence is checked up front so `-Installer` fails in a second rather than 45 minutes in. The default summary now points at `scripts/vousoir-shell-integration.ps1` | The installer is the only thing that registers the context menu, file associations and PATH; there was no way to build one | Low. `build.ps1` is a Vousoir file. | +| 18 | `build/next/index.ts`, `build/buildfile.ts`, `build/gulpfile.vscode.ts`, `build/gulpfile.vscode.web.ts`, `build/lib/mangle/index.ts` | Removed every build-manifest reference to the three trees Layer 2 physically deleted — `src/vs/sessions/`, `src/vs/platform/agentHost/`, `src/vs/platform/browserView/`. Specifically: 4 dead esbuild entry points plus the web-only `sessions.web.main.internal`, the sessions HTML/SVG/prompt/skill resource globs and `preload-browserView.ts` from `desktopStandaloneFiles` (all in `build/next/index.ts`, which `build/lib/esbuild.ts` spawns for **desktop as well as web** — it is the real bundler); 6 entry points and the `sessionsWeb` export in `buildfile.ts` and its two consumers; and in `gulpfile.vscode.ts` both the `sessions.html` / `preload-browserView.js` literals in `vscodeResourceIncludes` and 4 sessions paths in `computeChecksums` | **This is why no packaged build had ever succeeded.** The excision deleted the sources but never updated the manifests that enumerate them, so esbuild failed on unresolvable entry points. The two literal (non-glob) resource paths and the `computeChecksums` entries were latent second-stage failures — `computeChecksum` reads files directly and throws, so they would have killed the build *after* bundling had already run. Verified by `node build/next/index.ts bundle --target desktop`, which now completes (20 bundles, 104 resources), and by a full `vscode-win32-x64-min` reaching `package-win32-x64` successfully | Medium. These are upstream files under active change and our edits are deletions, so a merge will happily reintroduce the dead references; any future rebase must re-check that every manifest entry resolves. A cheap guard is to re-run the desktop bundle, which fails loudly on the first unresolvable entry point. | ## Layer 2 — Total AI / Microsoft-service excision (the hard divergence) diff --git a/vousoir/vendor/zoellner.openapi-preview-2.3.2.vsix b/vousoir/vendor/zoellner.openapi-preview-2.3.2.vsix new file mode 100644 index 00000000000..25f6718ab40 Binary files /dev/null and b/vousoir/vendor/zoellner.openapi-preview-2.3.2.vsix differ