Skip to content

Commit 491d689

Browse files
committed
fix(web): repair welcome and repository deep links
Use the reliable built-in ZCode-branded Welcome page instead of a CDN-hosted webview that cannot establish its isolated origin. Carry repository hash links through the browser workspace URI so the extension host opens the clone flow, then render the cloned files in the zcode-opfs Explorer tree.
1 parent 924cae2 commit 491d689

6 files changed

Lines changed: 161 additions & 13 deletions

File tree

‎PLAN.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
| **Repo** | [`github.com/spinupdev/zcode`](https://github.com/spinupdev/zcode) |
77
| **Local path** | may still be checked out as `code-server` — product is **ZCode** |
88
| **Document purpose** | Handoff for **any agent or engineer**: architecture, how systems connect, **done / in progress / remaining** |
9-
| **Last updated** | 2026-08-14 (repository deep-link handoff) |
9+
| **Last updated** | 2026-08-15 (welcome/webview and repository handoff) |
1010
| **Canonical design RFC** | [`docs/design-dual-mode-vscode-ide.md`](./docs/design-dual-mode-vscode-ide.md) |
1111
| **VS Code pin** | `1.129.0` → SHA `125df467…` ([`docs/vscode-pin.md`](./docs/vscode-pin.md)) |
1212
| **Status owner** | Update this file’s **Work tracker** whenever a work package finishes or starts |
@@ -459,5 +459,6 @@ pnpm smoke # lighter checks
459459
| 2026-07-27 | **WB8**: shell startup UX — WC prefetch + status bar + progress + auto-open terminal; Pyodide warm status |
460460
| 2026-07-27 | **WB8 fix**: main-thread `wc-bridge.js` + BroadcastChannel (web EH has no DOM); notification + bridge-based shell |
461461
| 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 |
462+
| 2026-08-15 | **B8f**: use the reliable built-in Welcome page on CDN-hosted VS Code Web; pass `/#https://...` repository handoffs through transient product configuration so the extension host receives them |
462463

463464
**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: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ try {
5050
}
5151

5252
const zcodeCoreExtensions = [
53-
{ path: '/extensions/zcode-welcome' },
5453
{ path: '/extensions/zcode-browser-fs' },
5554
{ path: '/extensions/zcode-git' },
5655
{ path: '/extensions/zcode-diagnostics' },
@@ -94,8 +93,8 @@ const defaultProduct = {
9493
configurationDefaults: {
9594
'security.workspace.trust.enabled': false,
9695
'security.workspace.trust.startupPrompt': 'never',
97-
// zcode-welcome owns the startup surface; do not open VS Code's Getting Started page.
98-
'workbench.startupEditor': 'none',
96+
// The built-in Welcome page renders reliably on CDN-hosted VS Code Web.
97+
'workbench.startupEditor': 'welcomePage',
9998
...themeConfigurationDefaults,
10099
'files.exclude': {
101100
'**/.git': true,
@@ -506,6 +505,71 @@ const bootstrap = `/* ZCode workbench bootstrap — load VS Code Web + inject ex
506505
} catch (_) { /* ignore */ }
507506
508507
window.product = withHostAuthority(window.product || {});
508+
// The extension host does not inherit the workbench page's URL fragment.
509+
// Pass a safe public repository URL through transient product defaults so
510+
// zcode-git can consume hash repository links deterministically.
511+
let startupRepository = '';
512+
try {
513+
const raw = (location.hash || '').slice(1).trim();
514+
if (/^https?:\\/\\//i.test(raw)) {
515+
const repo = new URL(decodeURIComponent(raw));
516+
if (
517+
(repo.protocol === 'http:' || repo.protocol === 'https:') &&
518+
repo.hostname &&
519+
repo.pathname !== '/' &&
520+
!repo.username &&
521+
!repo.password &&
522+
!repo.search &&
523+
!repo.hash
524+
) {
525+
startupRepository = repo.toString().replace(/#$/, '');
526+
history.replaceState(null, '', location.pathname + location.search);
527+
}
528+
}
529+
} catch (_) { /* malformed fragments stay inert */ }
530+
if (startupRepository) {
531+
if (window.product.folderUri?.scheme === 'zcode-opfs') {
532+
const startupBytes = Array.from(new TextEncoder().encode(startupRepository), (byte) =>
533+
String.fromCharCode(byte),
534+
).join('');
535+
const startupToken = btoa(startupBytes).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');
536+
window.product.folderUri = {
537+
...window.product.folderUri,
538+
path: '/workspace/__zcode_repo__' + startupToken,
539+
query: 'zcodeRepository=' + encodeURIComponent(startupRepository),
540+
};
541+
}
542+
try {
543+
sessionStorage.setItem('zcode.startupRepository', startupRepository);
544+
} catch (_) { /* private browsing can disable session storage */ }
545+
try {
546+
// localStorage is shared with the extension-host iframe; consume/remove
547+
// this immediately in zcode-git so it remains a one-shot handoff.
548+
localStorage.setItem('zcode.startupRepository', startupRepository);
549+
} catch (_) { /* private browsing can disable local storage */ }
550+
const topDefaults = window.product.configurationDefaults || {};
551+
const nestedDefaults = window.product.productConfiguration?.configurationDefaults || {};
552+
window.product.configurationDefaults = {
553+
...topDefaults,
554+
'zcode.startupRepository': startupRepository,
555+
};
556+
if (window.product.productConfiguration) {
557+
window.product.productConfiguration.configurationDefaults = {
558+
...nestedDefaults,
559+
'zcode.startupRepository': startupRepository,
560+
};
561+
}
562+
try {
563+
if (typeof BroadcastChannel !== 'undefined') {
564+
const channel = new BroadcastChannel('zcode.startup');
565+
const message = { type: 'openRepository', url: startupRepository };
566+
[0, 500, 1500, 3000].forEach((delay) => {
567+
setTimeout(() => channel.postMessage(message), delay);
568+
});
569+
setTimeout(() => channel.close(), 5000);
570+
}
571+
} catch (_) { /* extension host can still use product/storage handoff */ }
572+
}
509573
// Walkthrough / markdown webviews must load same-origin (not vscode-cdn.net).
510574
// Microsoft’s CDN blocks framing from non-Microsoft origins → “content is blocked”.
511575
const webviewEndpointPath =
@@ -751,7 +815,6 @@ writeFileSync(join(dist, 'bootstrap.js'), bootstrap);
751815
const extRoot = join(monorepo, 'extensions');
752816
const extOut = join(dist, 'extensions');
753817
const productExts = [
754-
'zcode-welcome',
755818
'zcode-browser-fs',
756819
'zcode-git',
757820
'zcode-diagnostics',

‎extensions/zcode-git/package.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@
168168
"default": "",
169169
"description": "isomorphic-git corsProxy base URL (empty = same-origin /git-proxy)"
170170
},
171+
"zcode.startupRepository": {
172+
"type": "string",
173+
"default": "",
174+
"description": "Transient repository URL supplied by a ZCode share link"
175+
},
171176
"zcode.gitToken": {
172177
"type": "string",
173178
"default": "",

‎extensions/zcode-git/src/extension.ts‎

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,55 @@ function readLastWorkspaceId(): string | undefined {
221221
}
222222
}
223223

224+
function readStartupRepository(): string | undefined {
225+
for (const folder of vscode.workspace.workspaceFolders ?? []) {
226+
if (folder.uri.scheme !== SCHEME) continue;
227+
const workspaceId = workspaceIdFromFolder(folder.uri);
228+
if (workspaceId?.startsWith('__zcode_repo__')) {
229+
try {
230+
const token = workspaceId.slice('__zcode_repo__'.length).replace(/-/g, '+').replace(/_/g, '/');
231+
const padded = token + '='.repeat((4 - (token.length % 4)) % 4);
232+
const bytes = Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
233+
const value = new TextDecoder().decode(bytes).trim();
234+
if (value) return value;
235+
} catch {
236+
/* malformed startup workspace token is ignored */
237+
}
238+
}
239+
try {
240+
const value = new URLSearchParams(folder.uri.query).get('zcodeRepository')?.trim();
241+
if (value) return value;
242+
} catch {
243+
/* malformed workspace query is ignored */
244+
}
245+
}
246+
247+
const configured = vscode.workspace
248+
.getConfiguration('zcode')
249+
.get<string>('startupRepository')
250+
?.trim();
251+
if (configured) return configured;
252+
try {
253+
const value = globalThis.sessionStorage?.getItem('zcode.startupRepository')?.trim();
254+
if (value) {
255+
globalThis.sessionStorage?.removeItem('zcode.startupRepository');
256+
return value;
257+
}
258+
} catch {
259+
/* private browsing can disable session storage */
260+
}
261+
try {
262+
const value = globalThis.localStorage?.getItem('zcode.startupRepository')?.trim();
263+
if (value) {
264+
globalThis.localStorage?.removeItem('zcode.startupRepository');
265+
return value;
266+
}
267+
} catch {
268+
/* private browsing can disable local storage */
269+
}
270+
return undefined;
271+
}
272+
224273
function writeLastWorkspaceId(id: string): void {
225274
try {
226275
globalThis.localStorage?.setItem(LAST_WS_KEY, id);
@@ -412,6 +461,7 @@ export function activate(context: vscode.ExtensionContext): void {
412461

413462
let refreshBusy = false;
414463
let openRepoBusy = false;
464+
let startupRepositoryHandled = false;
415465

416466
const ensureWorkspace = async (id: string): Promise<void> => {
417467
const a = await getAgent();
@@ -893,6 +943,15 @@ export function activate(context: vscode.ExtensionContext): void {
893943
() => undefined,
894944
);
895945

946+
// Workspace URI queries are only available after the provider/workspace
947+
// has settled; reading them earlier races VS Code Web startup.
948+
const configuredRepository = readStartupRepository();
949+
if (configuredRepository) {
950+
startupRepositoryHandled = true;
951+
await openRepository(configuredRepository);
952+
return;
953+
}
954+
896955
const hashRepository = repositoryUrlFromHash();
897956
if (hashRepository) {
898957
await openRepository(hashRepository);
@@ -944,7 +1003,7 @@ export function activate(context: vscode.ExtensionContext): void {
9441003
return;
9451004
}
9461005

947-
// zcode-welcome owns the empty-workspace startup surface.
1006+
// The built-in Welcome page owns the empty-workspace startup surface.
9481007
return;
9491008
} catch (err) {
9501009
console.warn('[zcode-git] startup flow', err);
@@ -1076,6 +1135,29 @@ export function activate(context: vscode.ExtensionContext): void {
10761135
}),
10771136
);
10781137

1138+
// The workbench page and web extension host do not share URL fragments.
1139+
// Receive the one-shot deep-link handoff over a same-origin channel.
1140+
try {
1141+
if (typeof BroadcastChannel !== 'undefined') {
1142+
const startupChannel = new BroadcastChannel('zcode.startup');
1143+
const onStartupMessage = (event: MessageEvent<{ type?: string; url?: string }>) => {
1144+
const url = event.data?.type === 'openRepository' ? event.data.url?.trim() : undefined;
1145+
if (!url || startupRepositoryHandled || validateGitUrl(url)) return;
1146+
startupRepositoryHandled = true;
1147+
void openRepository(url);
1148+
};
1149+
startupChannel.addEventListener('message', onStartupMessage);
1150+
context.subscriptions.push({
1151+
dispose: () => {
1152+
startupChannel.removeEventListener('message', onStartupMessage);
1153+
startupChannel.close();
1154+
},
1155+
});
1156+
}
1157+
} catch (err) {
1158+
console.warn('[zcode-git] startup repository channel unavailable', err);
1159+
}
1160+
10791161
void refresh();
10801162
const timer = setInterval(() => void refresh(), 8_000);
10811163
context.subscriptions.push({ dispose: () => clearInterval(timer) });

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

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

49-
it('embeds the ZCode welcome extension and language packs', () => {
49+
it('embeds the ZCode browser extensions 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'));
5352
assert.ok(paths.includes('/extensions/zcode-browser-fs'));
5453
assert.ok(paths.includes('/extensions/zcode-git'));
5554
assert.ok(paths.includes('/extensions/zcode-diagnostics'));
@@ -66,7 +65,7 @@ describe('buildWorkbenchCreateOptions', () => {
6665
assert.ok(paths.includes('/extensions/kotlin'));
6766
assert.ok(paths.includes('/extensions/solidity'));
6867
assert.ok(paths.includes('/extensions/zig'));
69-
assert.equal(o.productConfiguration.configurationDefaults?.['workbench.startupEditor'], 'none');
68+
assert.equal(o.productConfiguration.configurationDefaults?.['workbench.startupEditor'], 'welcomePage');
7069
// TextMate language packs (syntax highlighting)
7170
assert.ok(paths.includes('/vscode/extensions/javascript'));
7271
assert.ok(paths.includes('/vscode/extensions/typescript-basics'));

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ export interface BuildWorkbenchProductInput {
8989

9090
/** ZCode product extensions under /extensions/* */
9191
const ZCODE_PRODUCT_EXTENSIONS = [
92-
'/extensions/zcode-welcome',
9392
'/extensions/zcode-browser-fs',
9493
'/extensions/zcode-git',
9594
'/extensions/zcode-diagnostics',
@@ -144,9 +143,8 @@ export function configurationDefaultsForMode(
144143
const base: Record<string, unknown> = {
145144
'security.workspace.trust.enabled': false,
146145
'security.workspace.trust.startupPrompt': 'never',
147-
// Empty browser workspace — Welcome / Open Repository, not a fake README seed
148-
// zcode-welcome owns the startup surface; do not open VS Code's Getting Started page.
149-
'workbench.startupEditor': 'none',
146+
// Empty browser workspace — built-in Welcome / Open Repository, not a fake README seed.
147+
'workbench.startupEditor': 'welcomePage',
150148
// Built-in theme-defaults + seti (always available under /vscode/extensions)
151149
'workbench.iconTheme': ZCODE_THEME_DEFAULTS.iconTheme,
152150
'workbench.colorTheme': ZCODE_THEME_DEFAULTS.colorThemeDark,

0 commit comments

Comments
 (0)