Skip to content

Commit 924cae2

Browse files
committed
feat(workbench): replace VS Code welcome with ZCode repository opener
Add the zcode-welcome web extension with a focused HTTP(S) Git repository URL form, disable the bundled Getting Started page as the startup editor, and route submitted URLs through the existing browser clone flow. Update extension builds, deployment staging, shell defaults, and tracker coverage so the new welcome surface ships in local and Cloudflare builds.
1 parent 6353e09 commit 924cae2

13 files changed

Lines changed: 236 additions & 37 deletions

File tree

PLAN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ Update the **Status** column and **Last note** when you finish a package. Prefer
238238
| B8b | Welcome Open Repository → in-IDE HTTPS clone | **done** | `zcode.git.openRepository` + `remoteHub.openRepository` alias; notification progress; any public HTTPS host via `/git-proxy` `*`; Zeish favicon |
239239
| B8c | Multi-project browser workspaces + first-run clone | **done** | Unique workspace per clone; Browser Projects tree + manage QuickPick; last workspace restore (`localStorage` + `?workspace=`); `storage.persist`; startup clone prompt when empty; `/#https://...` opens a one-shot clone handoff |
240240
| B8d | Walkthrough media + ZCode branding | **done** | Same-origin `webviewEndpoint` (fix “content is blocked” from vscode-cdn); NLS brand script + `patches/0003-brand-walkthrough-zcode.patch`; docs in `docs/vscode-web.md` |
241+
| B8e | ZCode-owned welcome extension | **done** | Replaces VS Code Getting Started startup surface with HTTP(S) Git repository URL input; delegates clone to `zcode-git` |
241242
| B9 | SSH remotes / LFS / submodules | **deferred** | non-goals MVP |
242243
| B10 | Offline PWA | **deferred** | OQ7 |
243244

@@ -457,5 +458,6 @@ pnpm smoke # lighter checks
457458
| 2026-07-27 | **WB7**: browser integrated terminal via Pseudoterminal — WebContainer `jsh` + Pyodide REPL; terminal profiles; browser `terminal: true`; Open Browser Shell command |
458459
| 2026-07-27 | **WB8**: shell startup UX — WC prefetch + status bar + progress + auto-open terminal; Pyodide warm status |
459460
| 2026-07-27 | **WB8 fix**: main-thread `wc-bridge.js` + BroadcastChannel (web EH has no DOM); notification + bridge-based shell |
461+
| 2026-08-14 | **B8e**: added `zcode-welcome`; VS Code Getting Started is no longer the startup editor, and the ZCode welcome panel only accepts an HTTP(S) Git repository URL |
460462

461463
**When you complete work:** set the package **Status** to `done`, add a one-line **Last note** (commit SHA or PR), and append a row to §10.

apps/workbench/scripts/build.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ try {
5050
}
5151

5252
const zcodeCoreExtensions = [
53+
{ path: '/extensions/zcode-welcome' },
5354
{ path: '/extensions/zcode-browser-fs' },
5455
{ path: '/extensions/zcode-git' },
5556
{ path: '/extensions/zcode-diagnostics' },
@@ -93,7 +94,8 @@ const defaultProduct = {
9394
configurationDefaults: {
9495
'security.workspace.trust.enabled': false,
9596
'security.workspace.trust.startupPrompt': 'never',
96-
'workbench.startupEditor': 'welcomePage',
97+
// zcode-welcome owns the startup surface; do not open VS Code's Getting Started page.
98+
'workbench.startupEditor': 'none',
9799
...themeConfigurationDefaults,
98100
'files.exclude': {
99101
'**/.git': true,
@@ -749,6 +751,7 @@ writeFileSync(join(dist, 'bootstrap.js'), bootstrap);
749751
const extRoot = join(monorepo, 'extensions');
750752
const extOut = join(dist, 'extensions');
751753
const productExts = [
754+
'zcode-welcome',
752755
'zcode-browser-fs',
753756
'zcode-git',
754757
'zcode-diagnostics',

extensions/zcode-git/src/extension.ts

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ if (typeof g.Buffer === 'undefined') g.Buffer = Buffer;
3636

3737
const SCHEME = 'zcode-opfs';
3838
const LAST_WS_KEY = 'zcode.lastWorkspaceId';
39-
/** Skip auto welcome/clone prompt once per browser origin session after dismiss. */
40-
const SKIP_WELCOME_KEY = 'zcode.skipWelcomePrompt';
4139

4240
function workspaceIdFromFolder(uri: vscode.Uri): string | undefined {
4341
const parts = uri.path.replace(/^\/+/, '').split('/').filter(Boolean);
@@ -144,8 +142,8 @@ function validateGitUrl(raw: string): string | undefined {
144142
const t = raw.trim();
145143
if (!t) return 'URL required';
146144
const normalized = normalizeGitUrl(t);
147-
if (!/^https:\/\//i.test(normalized)) {
148-
return 'Use an https:// URL (browser clone does not support SSH remotes)';
145+
if (!/^https?:\/\//i.test(normalized)) {
146+
return 'Use an http:// or https:// URL (browser clone does not support SSH remotes)';
149147
}
150148
try {
151149
const u = new URL(normalized);
@@ -946,32 +944,8 @@ export function activate(context: vscode.ExtensionContext): void {
946944
return;
947945
}
948946

949-
// Truly empty install — prompt to clone (unless user dismissed this session)
950-
let skip = false;
951-
try {
952-
skip = globalThis.sessionStorage?.getItem(SKIP_WELCOME_KEY) === '1';
953-
} catch {
954-
/* ignore */
955-
}
956-
if (skip) return;
957-
958-
const choice = await vscode.window.showInformationMessage(
959-
'Welcome to ZCode. Clone a git repository into this browser (files persist in OPFS / IndexedDB).',
960-
'Open Repository…',
961-
'Browse Projects…',
962-
'Not now',
963-
);
964-
if (choice === 'Open Repository…') {
965-
await openRepository();
966-
} else if (choice === 'Browse Projects…') {
967-
await manageProjects();
968-
} else if (choice === 'Not now') {
969-
try {
970-
globalThis.sessionStorage?.setItem(SKIP_WELCOME_KEY, '1');
971-
} catch {
972-
/* ignore */
973-
}
974-
}
947+
// zcode-welcome owns the empty-workspace startup surface.
948+
return;
975949
} catch (err) {
976950
console.warn('[zcode-git] startup flow', err);
977951
}
@@ -1033,10 +1007,10 @@ export function activate(context: vscode.ExtensionContext): void {
10331007
}
10341008
}),
10351009
// Return promise so command stays active until clone finishes
1036-
vscode.commands.registerCommand('zcode.git.openRepository', () => openRepository()),
1037-
vscode.commands.registerCommand('zcode.git.clone', () => openRepository()),
1010+
vscode.commands.registerCommand('zcode.git.openRepository', (url?: string) => openRepository(url)),
1011+
vscode.commands.registerCommand('zcode.git.clone', (url?: string) => openRepository(url)),
10381012
// Welcome "Open Repository..." → ZCode clone (not Microsoft Remote Hub)
1039-
vscode.commands.registerCommand('remoteHub.openRepository', () => openRepository()),
1013+
vscode.commands.registerCommand('remoteHub.openRepository', (url?: string) => openRepository(url)),
10401014
vscode.commands.registerCommand('zcode.git.manageProjects', () => manageProjects()),
10411015
vscode.commands.registerCommand(
10421016
'zcode.git.openProject',
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "zcode-welcome",
3+
"displayName": "ZCode Welcome",
4+
"description": "ZCode's focused welcome experience for opening an HTTP(S) Git repository",
5+
"version": "0.1.0",
6+
"publisher": "spinupdev",
7+
"private": true,
8+
"engines": {
9+
"vscode": "^1.85.0"
10+
},
11+
"categories": [
12+
"Other"
13+
],
14+
"extensionKind": [
15+
"ui"
16+
],
17+
"browser": "./dist/web/extension.js",
18+
"activationEvents": [
19+
"onStartupFinished",
20+
"onCommand:zcode.welcome.open"
21+
],
22+
"contributes": {
23+
"commands": [
24+
{
25+
"command": "zcode.welcome.open",
26+
"title": "ZCode: Open Welcome"
27+
}
28+
]
29+
},
30+
"scripts": {
31+
"build": "node ./scripts/build.mjs",
32+
"typecheck": "tsc -p tsconfig.json --noEmit",
33+
"test": "node -e \"process.exit(0)\"",
34+
"clean": "rm -rf dist"
35+
},
36+
"devDependencies": {
37+
"@types/vscode": "^1.95.0",
38+
"esbuild": "^0.25.5",
39+
"typescript": "^5.8.3"
40+
}
41+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as esbuild from 'esbuild';
2+
import { mkdirSync } from 'node:fs';
3+
import { dirname, join } from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
6+
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
7+
mkdirSync(join(root, 'dist/web'), { recursive: true });
8+
9+
await esbuild.build({
10+
entryPoints: [join(root, 'src/extension.ts')],
11+
bundle: true,
12+
outfile: join(root, 'dist/web/extension.js'),
13+
format: 'cjs',
14+
platform: 'browser',
15+
target: ['es2022'],
16+
external: ['vscode'],
17+
logLevel: 'info',
18+
});
19+
20+
console.log('zcode-welcome: built dist/web/extension.js');
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import * as vscode from 'vscode';
2+
3+
let panel: vscode.WebviewPanel | undefined;
4+
5+
function isHttpGitUrl(value: string): boolean {
6+
try {
7+
const url = new URL(value.trim());
8+
return (url.protocol === 'http:' || url.protocol === 'https:') && !!url.hostname;
9+
} catch {
10+
return false;
11+
}
12+
}
13+
14+
function escapeHtml(value: string): string {
15+
return value.replace(/[&<>"']/g, (character) => {
16+
const entities: Record<string, string> = {
17+
'&': '&amp;',
18+
'<': '&lt;',
19+
'>': '&gt;',
20+
'"': '&quot;',
21+
"'": '&#39;',
22+
};
23+
return entities[character] ?? character;
24+
});
25+
}
26+
27+
function welcomeHtml(webview: vscode.Webview): string {
28+
const nonce = `${Date.now()}${Math.random().toString(36).slice(2)}`;
29+
return `<!doctype html>
30+
<html lang="en">
31+
<head>
32+
<meta charset="UTF-8">
33+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
34+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';">
35+
<style>
36+
:root { color-scheme: light dark; }
37+
body { max-width: 680px; margin: 0 auto; padding: 12vh 32px; color: var(--vscode-foreground); font-family: var(--vscode-font-family); }
38+
h1 { font-size: 30px; font-weight: 600; margin: 0 0 8px; }
39+
p { color: var(--vscode-descriptionForeground); margin: 0 0 28px; }
40+
label { display: block; font-weight: 600; margin-bottom: 8px; }
41+
.row { display: flex; gap: 8px; }
42+
input { flex: 1; min-width: 0; padding: 9px 10px; color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); border-radius: 3px; font: inherit; }
43+
button { padding: 9px 16px; color: var(--vscode-button-foreground); background: var(--vscode-button-background); border: 0; border-radius: 3px; font: inherit; cursor: pointer; }
44+
button:hover { background: var(--vscode-button-hoverBackground); }
45+
#error { min-height: 20px; margin-top: 10px; color: var(--vscode-errorForeground); }
46+
</style>
47+
</head>
48+
<body>
49+
<h1>Welcome to ZCode</h1>
50+
<p>Paste an HTTP or HTTPS Git repository URL to start working in your browser.</p>
51+
<form id="form">
52+
<label for="url">Git repository URL</label>
53+
<div class="row">
54+
<input id="url" type="url" autocomplete="url" placeholder="https://github.com/org/repo.git" autofocus>
55+
<button type="submit">Open Repository</button>
56+
</div>
57+
<div id="error" role="alert"></div>
58+
</form>
59+
<script nonce="${nonce}">
60+
const vscode = acquireVsCodeApi();
61+
const form = document.getElementById('form');
62+
const input = document.getElementById('url');
63+
const error = document.getElementById('error');
64+
form.addEventListener('submit', (event) => {
65+
event.preventDefault();
66+
const value = input.value.trim();
67+
try {
68+
const url = new URL(value);
69+
if (!['http:', 'https:'].includes(url.protocol) || !url.hostname) throw new Error();
70+
error.textContent = '';
71+
vscode.postMessage({ type: 'openRepository', url: value });
72+
} catch {
73+
error.textContent = 'Enter a valid HTTP(S) Git repository URL.';
74+
input.focus();
75+
}
76+
});
77+
</script>
78+
</body>
79+
</html>`;
80+
}
81+
82+
function openWelcome(context: vscode.ExtensionContext): void {
83+
if (panel) {
84+
panel.reveal(vscode.ViewColumn.One);
85+
return;
86+
}
87+
88+
panel = vscode.window.createWebviewPanel(
89+
'zcode.welcome',
90+
'Welcome to ZCode',
91+
vscode.ViewColumn.One,
92+
{ enableScripts: true, retainContextWhenHidden: true },
93+
);
94+
panel.webview.html = welcomeHtml(panel.webview);
95+
panel.onDidDispose(
96+
() => {
97+
panel = undefined;
98+
},
99+
null,
100+
context.subscriptions,
101+
);
102+
panel.webview.onDidReceiveMessage(
103+
async (message: { type?: string; url?: string }) => {
104+
if (message.type !== 'openRepository' || typeof message.url !== 'string') return;
105+
const url = message.url.trim();
106+
if (!isHttpGitUrl(url)) {
107+
void vscode.window.showErrorMessage('Enter a valid HTTP(S) Git repository URL.');
108+
return;
109+
}
110+
await vscode.commands.executeCommand('zcode.git.openRepository', url);
111+
},
112+
null,
113+
context.subscriptions,
114+
);
115+
}
116+
117+
export function activate(context: vscode.ExtensionContext): void {
118+
context.subscriptions.push(
119+
vscode.commands.registerCommand('zcode.welcome.open', () => openWelcome(context)),
120+
);
121+
openWelcome(context);
122+
}
123+
124+
export function deactivate(): void {
125+
panel = undefined;
126+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ESNext",
5+
"moduleResolution": "Bundler",
6+
"lib": ["ES2022", "DOM"],
7+
"strict": true,
8+
"noEmit": true,
9+
"skipLibCheck": true,
10+
"types": ["vscode"]
11+
},
12+
"include": ["src/**/*"]
13+
}

packages/shell/src/workbench-product.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ describe('buildWorkbenchCreateOptions', () => {
4646
assert.equal(o.connectionReady, true);
4747
});
4848

49-
it('embeds default builtin extension paths including diagnostics + language packs', () => {
49+
it('embeds the ZCode welcome extension and language packs', () => {
5050
const o = buildWorkbenchCreateOptions({ mode: 'browser' });
5151
const paths = o.additionalBuiltinExtensions?.map((e) => e.path) ?? [];
52+
assert.ok(paths.includes('/extensions/zcode-welcome'));
5253
assert.ok(paths.includes('/extensions/zcode-browser-fs'));
5354
assert.ok(paths.includes('/extensions/zcode-git'));
5455
assert.ok(paths.includes('/extensions/zcode-diagnostics'));
@@ -65,6 +66,7 @@ describe('buildWorkbenchCreateOptions', () => {
6566
assert.ok(paths.includes('/extensions/kotlin'));
6667
assert.ok(paths.includes('/extensions/solidity'));
6768
assert.ok(paths.includes('/extensions/zig'));
69+
assert.equal(o.productConfiguration.configurationDefaults?.['workbench.startupEditor'], 'none');
6870
// TextMate language packs (syntax highlighting)
6971
assert.ok(paths.includes('/vscode/extensions/javascript'));
7072
assert.ok(paths.includes('/vscode/extensions/typescript-basics'));

packages/shell/src/workbench-product.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export interface BuildWorkbenchProductInput {
8989

9090
/** ZCode product extensions under /extensions/* */
9191
const ZCODE_PRODUCT_EXTENSIONS = [
92+
'/extensions/zcode-welcome',
9293
'/extensions/zcode-browser-fs',
9394
'/extensions/zcode-git',
9495
'/extensions/zcode-diagnostics',
@@ -144,7 +145,8 @@ export function configurationDefaultsForMode(
144145
'security.workspace.trust.enabled': false,
145146
'security.workspace.trust.startupPrompt': 'never',
146147
// Empty browser workspace — Welcome / Open Repository, not a fake README seed
147-
'workbench.startupEditor': 'welcomePage',
148+
// zcode-welcome owns the startup surface; do not open VS Code's Getting Started page.
149+
'workbench.startupEditor': 'none',
148150
// Built-in theme-defaults + seti (always available under /vscode/extensions)
149151
'workbench.iconTheme': ZCODE_THEME_DEFAULTS.iconTheme,
150152
'workbench.colorTheme': ZCODE_THEME_DEFAULTS.colorThemeDark,

pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)