Skip to content

Commit 72383a0

Browse files
ndemiancclaude
andcommitted
fix(ui): show the LevelCode release version in the update tooltip and About
The update tooltip read "Current Version: 1.126.0 (cdf2549)" — a Code-OSS version next to a LevelCode commit — and "Released 22 Jun 2026", the upstream base's build date rather than the release the user installed. Reported after the first real auto-update, where it reads as a different product entirely. The commit was already right on purpose (build-macos.sh stamps LevelCode's HEAD so releases are distinguishable to the update feed). Only the version and date were still Code-OSS's, and `version` cannot simply be renamed: it is what extensions' `engines.vscode` is validated against, so setting it to 0.8.0 would reject every extension requiring ^1.x. So the release identity rides alongside it: - scripts/stamp-levelcode-version.mjs (new) stamps `levelcodeVersion` + `levelcodeReleaseDate` into the BUILT app's product.json. Refuses anything that isn't a plain dotted version (exit 1), so a bad `git describe` fails the build rather than shipping "Current Version: v0.8". - build-macos.sh runs it tag-derived, after the existing strip steps. No reachable tag (dev builds) → skipped, and the UI falls back to `version`. - The tooltip shows `levelcodeVersion ?? version`. About shows BOTH ("0.8.0 — Code-OSS 1.126.0") since it gets pasted into bug reports, where the base version explains extension-compatibility behaviour. Patch-workflow note: vscode/ is gitignored, so the three core edits are captured in patches/levelcode-core.patch (11 -> 14 entries). Regenerating wholesale is NOT safe here — the checkout is already de-branded, and a full regen pulled ~95 lines of de-brand's MS-doc-link stripping into the files.contribution.ts entry. The existing entries are preserved byte-identical and only the three new ones appended. CORE-PATCHES.md gains that escape hatch as NOTE 3, plus rows 12-14. Verified: typecheck-client passes (exit 0); the stamp script leaves `version` untouched, strips a v prefix, and exits 1 on a bad version; the 14-entry patch reverse-applies cleanly to the checkout; the appended entries carry no de-brand contamination and 6 [LevelCode] markers. NOT verified: the rendered strings. That needs a full gulp build and a launched app — see the PR for the check to run before the next release. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 876afa9 commit 72383a0

4 files changed

Lines changed: 199 additions & 1 deletion

File tree

docs/CORE-PATCHES.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ So a full rebuild from nothing is: `bootstrap.sh` (clone → brand → extension
2626
To re-create the core patch after changing core files in `vscode/`:
2727

2828
```bash
29-
# STRUCTURAL patch only (11 files). Display-string rebrands are NOT here — they live in scripts/de-brand.mjs.
29+
# STRUCTURAL patch only (14 files). Display-string rebrands are NOT here — they live in scripts/de-brand.mjs.
3030
git -C vscode diff HEAD -- \
3131
src/vs/workbench/contrib/files/browser/files.contribution.ts \
3232
build/lib/extensions.ts build/lib/copilot.ts \
@@ -38,12 +38,22 @@ git -C vscode diff HEAD -- \
3838
src/vs/workbench/contrib/welcomeOnboarding/browser/welcomeOnboarding.contribution.ts \
3939
src/vs/workbench/api/node/loopbackServer.ts \
4040
src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts \
41+
src/vs/base/common/product.ts \
42+
src/vs/platform/dialogs/electron-browser/dialog.ts \
43+
src/vs/workbench/contrib/update/browser/updateTooltip.ts \
4144
> patches/levelcode-core.patch
4245
# NOTE 1: use `diff HEAD` (not plain `diff`) — bootstrap's `git apply` may leave these STAGED,
4346
# and plain `git diff` shows only UNSTAGED changes, silently dropping the staged patches.
4447
# NOTE 2: regenerate BEFORE running de-brand.mjs. de-brand's global MS-doc-link sweep (pass 2) also
4548
# strips links from `files.contribution.ts` (a patched file); if you regen after de-brand, those
4649
# link-strips leak into the patch. Order: patch → regen → de-brand (this is also bootstrap's order).
50+
# NOTE 3: if the checkout is ALREADY de-branded (the usual case after a build) do NOT regen wholesale —
51+
# it silently fattened `files.contribution.ts` by ~95 lines of link-strips the one time we tried. Instead
52+
# APPEND only the new file's entry to the existing patch, which stays valid because bootstrap applies it
53+
# to a fresh, pre-de-brand checkout:
54+
# git -C vscode diff HEAD -- <only/the/new/file.ts> >> patches/levelcode-core.patch
55+
# Then verify: the pre-existing entries are byte-identical to before, the appended entry contains no
56+
# `aka.ms`/`code.visualstudio.com` strips, and `git -C vscode apply --check --reverse` accepts it.
4757
```
4858

4959
To find every core touch in the checkout:
@@ -74,6 +84,10 @@ compile-client` after touching a core file.**
7484
| 10 | `api/node/loopbackServer.ts` (`getHtml`) | Replace the `this._appName === 'Visual Studio Code'` / `'… - Insiders'` branches (which embedded the VS Code stable/Insiders **shields**, falling through to the blue VS Code **"book"** default) with a single **LevelCode chevron** data-URI. | De-brand (WS-C/L5). A latent bug **and** a leak: `appName` is now "LevelCode" so no branch matched → every GitHub OAuth success page flashed a VS Code logo. `this._appName` is still used in the page text, so no unused-field error. |
7585
| 11 | `chat/browser/chatSetup/chatSetupContributions.ts` | **Hide** the two GitHub Copilot sign-in call-to-actions (Accounts menu + title bar) with `when: ContextKeyExpr.false()`, and drop the two imports that becomes unused (`ChatEntitlementContextKeys`, `InEditorZenModeContext` — strict build). | De-brand (WS-A/B4). A sign-in funnel for an extension we don't ship. **Gated per MENU ITEM on purpose.** The previous attempt forced `IChatEntitlementService.setForceHidden(true)` instead — but `Setup.hidden` is upstream's *hide-ALL-chat/agent-UI* flag, not a CTA gate: it also gates `OPEN_AGENTS_WINDOW_PRECONDITION` (constants.ts), the **Agent Plugins view**, chat participants and several chat/plugin actions. That shipped in v0.6.0–v0.7.0 and made **Open Agents Window disappear from the Command Palette**. Never gate a de-brand on `Setup.hidden`; gate the item. |
7686

87+
| 12 | `src/vs/base/common/product.ts` | Add optional `levelcodeVersion` + `levelcodeReleaseDate` to `IProductConfiguration`. | `version` is the Code-OSS base (`1.126.0`) and **must stay 1.x** — it is what extensions' `engines.vscode` is validated against, so it cannot be renamed to the LevelCode release. These carry the human-facing release identity alongside it. Stamped into the built `product.json` by `scripts/stamp-levelcode-version.mjs` (run from `build-macos.sh`, tag-derived); both optional, so a dev build with no reachable tag still renders. |
88+
| 13 | `update/browser/updateTooltip.ts` | `Current Version:` shows `levelcodeVersion ?? version`; `Released` prefers `levelcodeReleaseDate`. | The update tooltip read **“Current Version: 1.126.0 (cdf2549)” — a Code-OSS version next to a LevelCode commit**, and a “Released” date from the upstream base's build (months before the build the user installed). Reported as counter-intuitive after the first real auto-update. `Latest Version:` already used the feed's `productVersion` and needed no change. |
89+
| 14 | `platform/dialogs/electron-browser/dialog.ts` | About shows `0.8.0 … — Code-OSS 1.126.0`, and the Date row prefers `levelcodeReleaseDate`. | Same root cause as #13, on the native About dialog. Keeps **both** here on purpose: About is pasted into bug reports, so the release version identifies the build while the base version explains extension-compatibility behaviour. |
90+
7791
## String + link rebrands — `scripts/de-brand.mjs` (NOT in the patch)
7892

7993
Content-based replacements — matched on the string, not its line number — so they survive upstream line

patches/levelcode-core.patch

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,102 @@ index 1a1f1e1d..56af75ac 100644
290290
- order: 3
291291
-});
292292
+// [LevelCode] Removed the Help ▸ "Editor Playground" menu item (Microsoft VS Code walkthrough content).
293+
diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts
294+
index 5a1a2018..36b7c03a 100644
295+
--- a/src/vs/base/common/product.ts
296+
+++ b/src/vs/base/common/product.ts
297+
@@ -89,6 +89,23 @@ export interface IProductConfiguration {
298+
readonly quality?: string;
299+
readonly commit?: string;
300+
301+
+ /**
302+
+ * [LevelCode] The LevelCode RELEASE version ("0.8.0"), stamped at build time from the git tag.
303+
+ *
304+
+ * Distinct from `version`, which is and must remain the Code-OSS base ("1.126.0") — that is the
305+
+ * value extensions' `engines.vscode` is validated against, so renaming it would reject every
306+
+ * extension requiring ^1.x. Use this for anything a HUMAN reads (update UI, About); use `version`
307+
+ * for compatibility checks. Absent in dev builds, so always fall back to `version`.
308+
+ */
309+
+ readonly levelcodeVersion?: string;
310+
+
311+
+ /**
312+
+ * [LevelCode] When this build's commit was authored (ISO 8601), stamped alongside `levelcodeVersion`.
313+
+ * `date` is the Code-OSS build date and is misleading in release UI — it reflects the upstream base,
314+
+ * not when this LevelCode release shipped.
315+
+ */
316+
+ readonly levelcodeReleaseDate?: string;
317+
+
318+
readonly nameShort: string;
319+
readonly nameLong: string;
320+
321+
diff --git a/src/vs/platform/dialogs/electron-browser/dialog.ts b/src/vs/platform/dialogs/electron-browser/dialog.ts
322+
index ec26804c..766198c2 100644
323+
--- a/src/vs/platform/dialogs/electron-browser/dialog.ts
324+
+++ b/src/vs/platform/dialogs/electron-browser/dialog.ts
325+
@@ -11,19 +11,33 @@ import { IProductService } from '../../product/common/productService.js';
326+
import { process } from '../../../base/parts/sandbox/electron-browser/globals.js';
327+
328+
export function createNativeAboutDialogDetails(productService: IProductService, osProps: IOSProperties): { title: string; details: string; detailsToCopy: string } {
329+
- let version = productService.version;
330+
+ // [LevelCode] Lead with the RELEASE version — that is what a user installed and what the release,
331+
+ // the update feed and the download page all call it. `productService.version` is the Code-OSS base
332+
+ // ("1.126.0") and must stay 1.x for extensions' engines.vscode, so it cannot simply be renamed.
333+
+ // About is a diagnostics surface people paste into bug reports, so keep BOTH: the release version
334+
+ // identifies the build, the base version explains extension-compatibility behaviour. Dev builds
335+
+ // carry no stamp and fall back to showing the base alone.
336+
+ let version = productService.levelcodeVersion ?? productService.version;
337+
if (productService.target) {
338+
version = `${version} (${productService.target} setup)`;
339+
} else if (productService.darwinUniversalAssetId) {
340+
version = `${version} (Universal)`;
341+
}
342+
+ if (productService.levelcodeVersion) {
343+
+ version = `${version} — Code-OSS ${productService.version}`;
344+
+ }
345+
346+
const getDetails = (useAgo: boolean): string => {
347+
return localize({ key: 'aboutDetail', comment: ['Electron, Chromium, Node.js and V8 are product names that need no translation'] },
348+
"Version: {0}\nCommit: {1}\nDate: {2}\nElectron: {3}\nElectronBuildId: {4}\nChromium: {5}\nNode.js: {6}\nV8: {7}\nOS: {8}",
349+
version,
350+
productService.commit || 'Unknown',
351+
- productService.date ? `${productService.date}${useAgo ? ' (' + fromNow(new Date(productService.date), true) + ')' : ''}` : 'Unknown',
352+
+ // [LevelCode] Same reasoning as the version: `date` is the Code-OSS base's build date, reading as a
353+
+ // release date months before the build the user actually installed.
354+
+ (() => {
355+
+ const d = productService.levelcodeReleaseDate ?? productService.date;
356+
+ return d ? `${d}${useAgo ? ' (' + fromNow(new Date(d), true) + ')' : ''}` : 'Unknown';
357+
+ })(),
358+
process.versions['electron'],
359+
process.versions['microsoft-build'],
360+
process.versions['chrome'],
361+
diff --git a/src/vs/workbench/contrib/update/browser/updateTooltip.ts b/src/vs/workbench/contrib/update/browser/updateTooltip.ts
362+
index 85e34cca..3f9b67ba 100644
363+
--- a/src/vs/workbench/contrib/update/browser/updateTooltip.ts
364+
+++ b/src/vs/workbench/contrib/update/browser/updateTooltip.ts
365+
@@ -137,7 +137,11 @@ export class UpdateTooltip extends Disposable {
366+
}
367+
368+
private updateCurrentVersion() {
369+
- const productVersion = this.productService.version;
370+
+ // [LevelCode] Show the RELEASE version ("0.8.0"), never `version` — that is the Code-OSS base
371+
+ // ("1.126.0"), which must stay 1.x for extensions' engines.vscode. Pairing it with a LevelCode
372+
+ // commit made this read as a different product than the one that was installed. Dev builds carry
373+
+ // no stamp, so fall back rather than showing nothing.
374+
+ const productVersion = this.productService.levelcodeVersion ?? this.productService.version;
375+
if (productVersion) {
376+
const currentCommitId = this.productService.commit?.substring(0, 7);
377+
this.currentVersionNode.textContent = currentCommitId
378+
@@ -381,8 +385,11 @@ export class UpdateTooltip extends Disposable {
379+
this.latestVersionNode.parentElement!.style.display = 'none';
380+
}
381+
382+
- // Release date
383+
- const releaseDate = update?.timestamp ?? tryParseDate(this.productService.date);
384+
+ // [LevelCode] Release date: for the running build prefer the LevelCode date — `date` is the Code-OSS
385+
+ // base's build date, so it reported a "Released" months before the release the user installed.
386+
+ const releaseDate = update?.timestamp
387+
+ ?? tryParseDate(this.productService.levelcodeReleaseDate)
388+
+ ?? tryParseDate(this.productService.date);
389+
if (typeof releaseDate === 'number' && releaseDate > 0) {
390+
this.releaseDateNode.textContent = localize('updateTooltip.releasedLabel', "Released {0}", formatDate(releaseDate));
391+
this.releaseDateNode.style.display = '';

scripts/build-macos.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,22 @@ node "$SCRIPT_DIR/strip-proprietary.mjs" "$BUILT_APP/LevelCode.app/Contents/Reso
8585
echo "[build] Hiding not-yet-ready features from the app (LevelCode Sync) …"
8686
node "$SCRIPT_DIR/strip-unreleased.mjs" "$BUILT_APP/LevelCode.app/Contents/Resources/app"
8787

88+
# Stamp the LevelCode RELEASE version for anything a human reads (update tooltip, About). product.json
89+
# `version` deliberately stays the Code-OSS base — it is what extensions' engines.vscode is validated
90+
# against — so the release version rides ALONGSIDE it rather than replacing it. Without this the update
91+
# UI reports "1.126.0", the upstream base, next to a LevelCode commit. See stamp-levelcode-version.mjs.
92+
# Tag-derived, so a CI build at tag vX.Y.Z stamps X.Y.Z; a dev build with no reachable tag skips it and
93+
# the UI falls back to `version`.
94+
if LC_VERSION="$(git -C "$ROOT_DIR" describe --tags --abbrev=0 2>/dev/null)" && [ -n "$LC_VERSION" ]; then
95+
LC_DATE="$(git -C "$ROOT_DIR" log -1 --format=%cI HEAD 2>/dev/null || true)"
96+
echo "[build] Stamping the LevelCode release version ($LC_VERSION) …"
97+
node "$SCRIPT_DIR/stamp-levelcode-version.mjs" \
98+
"$BUILT_APP/LevelCode.app/Contents/Resources/app" "$LC_VERSION" "$LC_DATE"
99+
else
100+
echo "[build] WARN: no reachable git tag — skipping the release-version stamp. The update UI will"
101+
echo "[build] show the Code-OSS base version instead of a LevelCode one."
102+
fi
103+
88104
# The .app inside is named from product.json nameLong -> "LevelCode.app".
89105
echo "[build] Done."
90106
echo "[build] Output folder: $BUILT_APP"
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* LevelCode — stamp the RELEASE version into the BUILT app's product.json.
3+
*
4+
* Usage: node stamp-levelcode-version.mjs <app>/Contents/Resources/app <version> [isoDate]
5+
*
6+
* WHY THIS EXISTS
7+
* product.json `version` is, and must remain, the Code-OSS base ("1.126.0"). That value is what
8+
* extensions' `engines.vscode` is validated against — set it to "0.8.0" and every extension
9+
* requiring ^1.x is rejected. But it is also what the update UI and About dialog display, so a
10+
* LevelCode build reported itself as "1.126.0", released on the upstream base's build date, while
11+
* carrying a LevelCode commit. Half Code-OSS, half LevelCode, and confusing either way.
12+
*
13+
* So we add SEPARATE, human-facing fields rather than overloading `version`:
14+
* levelcodeVersion "0.8.0" ← from the git tag
15+
* levelcodeReleaseDate "2026-07-20T02:51:22Z" ← when this build's commit was authored
16+
*
17+
* Compatibility checks keep reading `version`; humans read these. Both are optional in the type,
18+
* so a dev build with neither still renders (it falls back to `version`).
19+
*
20+
* Idempotent + loud: re-stamping overwrites, and every outcome is printed.
21+
*--------------------------------------------------------------------------------------------*/
22+
import fs from "node:fs";
23+
import path from "node:path";
24+
25+
const appDir = process.argv[2];
26+
const version = process.argv[3];
27+
const isoDate = process.argv[4];
28+
29+
if (!appDir || !version) {
30+
console.error("[stamp] ERROR: usage: stamp-levelcode-version.mjs <.../Resources/app> <version> [isoDate]");
31+
process.exit(1);
32+
}
33+
if (!fs.existsSync(appDir)) {
34+
console.error(`[stamp] ERROR: ${appDir} not found — pass the built app's Contents/Resources/app dir.`);
35+
process.exit(1);
36+
}
37+
38+
// A release version, not a tag: "v0.8.0" would render as "Current Version: v0.8.0". Reject anything
39+
// that isn't a plain dotted version so a bad --describe result can't ship as the product version.
40+
const clean = String(version).replace(/^v/, "");
41+
if (!/^\d+\.\d+\.\d+([-+].+)?$/.test(clean)) {
42+
console.error(`[stamp] ERROR: "${version}" is not a version like 0.8.0 — refusing to stamp.`);
43+
process.exit(1);
44+
}
45+
46+
const productPath = path.join(appDir, "product.json");
47+
try {
48+
const product = JSON.parse(fs.readFileSync(productPath, "utf8"));
49+
const previous = product.levelcodeVersion;
50+
51+
product.levelcodeVersion = clean;
52+
if (isoDate && !Number.isNaN(Date.parse(isoDate))) {
53+
product.levelcodeReleaseDate = new Date(isoDate).toISOString();
54+
} else if (isoDate) {
55+
console.warn(`[stamp] WARN: ignoring unparseable date "${isoDate}" — the UI will fall back to product.date.`);
56+
}
57+
58+
fs.writeFileSync(productPath, JSON.stringify(product, null, "\t") + "\n");
59+
console.log(
60+
`[stamp] product.json levelcodeVersion = ${clean}` +
61+
(product.levelcodeReleaseDate ? `, levelcodeReleaseDate = ${product.levelcodeReleaseDate}` : "") +
62+
(previous && previous !== clean ? ` (was ${previous})` : "")
63+
);
64+
// `version` stays put on purpose — see the header.
65+
console.log(`[stamp] product.json version left at ${product.version} (Code-OSS base, for engines.vscode).`);
66+
} catch (e) {
67+
console.error(`[stamp] ERROR: could not stamp ${productPath}: ${(e && e.message) || e}`);
68+
process.exit(1);
69+
}

0 commit comments

Comments
 (0)