diff --git a/apps/web/src/components/Icons.tsx b/apps/web/src/components/Icons.tsx index fc5a37fc67fb..59a91c618d31 100644 --- a/apps/web/src/components/Icons.tsx +++ b/apps/web/src/components/Icons.tsx @@ -700,25 +700,16 @@ export const ACPRegistryIcon: Icon = ({ className, ...props }) => ( export const MtModelIcon: Icon = ({ className, ...props }) => ( + {/* The Munim mark, same path the app icon draws. */} - - ); diff --git a/apps/web/src/components/SidebarStageBackdrop.test.tsx b/apps/web/src/components/SidebarStageBackdrop.test.tsx index eca741af8c89..4feb4b2f21c7 100644 --- a/apps/web/src/components/SidebarStageBackdrop.test.tsx +++ b/apps/web/src/components/SidebarStageBackdrop.test.tsx @@ -2,7 +2,10 @@ import { describe, expect, it } from "vite-plus/test"; import { renderToStaticMarkup } from "react-dom/server"; import { + DEV_BACKDROP, + NIGHTLY_BACKDROP, resolveEnvironmentIdentificationPillLabel, + resolveSidebarArtwork, resolveSidebarStageBackdropVariant, resolveSidebarStageFocusRingOffsetClass, StageBackdropArt, @@ -11,8 +14,8 @@ import { describe("SidebarStageBackdrop", () => { it("resolves stage artwork only when enabled", () => { - expect(resolveSidebarStageBackdropVariant("Dev")).toBe("dev"); - expect(resolveSidebarStageBackdropVariant("Nightly")).toBe("nightly"); + expect(resolveSidebarStageBackdropVariant("Dev")).toEqual(DEV_BACKDROP); + expect(resolveSidebarStageBackdropVariant("Nightly")).toEqual(NIGHTLY_BACKDROP); expect(resolveSidebarStageBackdropVariant("Dev", false)).toBeNull(); expect(resolveSidebarStageBackdropVariant("Alpha")).toBeNull(); }); @@ -25,16 +28,16 @@ describe("SidebarStageBackdrop", () => { }); it("matches the focus-ring offset to each artwork palette", () => { - expect(resolveSidebarStageFocusRingOffsetClass("nightly")).toBe( + expect(resolveSidebarStageFocusRingOffsetClass(NIGHTLY_BACKDROP)).toBe( "focus-visible:ring-offset-(--stage-night-bottom)", ); - expect(resolveSidebarStageFocusRingOffsetClass("dev")).toBe( + expect(resolveSidebarStageFocusRingOffsetClass(DEV_BACKDROP)).toBe( "focus-visible:ring-offset-(--stage-art-bottom)", ); }); - it.each(["nightly", "dev"] as const)( - "uses unique SVG definition ids when %s artwork is rendered more than once", + it.each([NIGHTLY_BACKDROP, DEV_BACKDROP] as const)( + "uses unique SVG definition ids when $kind artwork is rendered more than once", (variant) => { const markup = renderToStaticMarkup( <> @@ -50,8 +53,8 @@ describe("SidebarStageBackdrop", () => { ); it("paints each artwork variant with theme-owned color tokens", () => { - const nightlyMarkup = renderToStaticMarkup(); - const devMarkup = renderToStaticMarkup(); + const nightlyMarkup = renderToStaticMarkup(); + const devMarkup = renderToStaticMarkup(); expect(nightlyMarkup).toContain("var(--stage-night-bottom)"); expect(nightlyMarkup).toContain("var(--stage-night-line)"); @@ -62,12 +65,49 @@ describe("SidebarStageBackdrop", () => { }); it.each([ - ["nightly", "96 0 8192 96"], - ["dev", "64 0 8192 96"], - ] as const)("uses the compact %s crop inside the send button", (variant, viewBox) => { + [NIGHTLY_BACKDROP, "96 0 8192 96"], + [DEV_BACKDROP, "64 0 8192 96"], + ] as const)("uses the compact crop inside the send button", (variant, viewBox) => { const markup = renderToStaticMarkup(); expect(markup).toContain(`viewBox="${viewBox}"`); - expect(markup).toContain(`stage-${variant === "dev" ? "blueprint" : "nightly"}`); + expect(markup).toContain(`stage-${variant.kind === "dev" ? "blueprint" : "nightly"}`); + }); +}); + +describe("resolveSidebarArtwork", () => { + const custom = [{ id: "art_1", name: "Skyline", image: "data:image/png;base64,AAAA" }]; + + it("defers to the build channel only for auto", () => { + expect(resolveSidebarArtwork({ selection: "auto", stageLabel: "Nightly", custom })).toEqual( + NIGHTLY_BACKDROP, + ); + // A release build has no channel artwork, which is why picking one matters. + expect(resolveSidebarArtwork({ selection: "auto", stageLabel: "", custom })).toBeNull(); + }); + + it("honours an explicit pick regardless of channel", () => { + expect(resolveSidebarArtwork({ selection: "night", stageLabel: "", custom })).toEqual( + NIGHTLY_BACKDROP, + ); + expect(resolveSidebarArtwork({ selection: "day", stageLabel: "Nightly", custom })).toEqual( + DEV_BACKDROP, + ); + expect(resolveSidebarArtwork({ selection: "none", stageLabel: "Nightly", custom })).toBeNull(); + }); + + it("renders the account's own artwork, and nothing once it is deleted", () => { + expect(resolveSidebarArtwork({ selection: "art_1", stageLabel: "", custom })).toEqual({ + kind: "custom", + image: custom[0]!.image, + name: custom[0]!.name, + }); + expect(resolveSidebarArtwork({ selection: "art_1", stageLabel: "", custom: [] })).toBeNull(); + }); + + it("stays out of the way when artwork is switched off entirely", () => { + expect( + resolveSidebarArtwork({ selection: "night", stageLabel: "", custom, enabled: false }), + ).toBeNull(); }); }); diff --git a/apps/web/src/components/SidebarStageBackdrop.tsx b/apps/web/src/components/SidebarStageBackdrop.tsx index 772017a6d086..d45a77884bd2 100644 --- a/apps/web/src/components/SidebarStageBackdrop.tsx +++ b/apps/web/src/components/SidebarStageBackdrop.tsx @@ -4,10 +4,44 @@ import { useId } from "react"; import { APP_HAS_UPDATE_TRACKS, APP_STAGE_LABEL } from "../branding"; import { resolveServerBackedAppStageLabel } from "../branding.logic"; import { primaryServerConfigAtom } from "../state/server"; +import { usePrimarySettings } from "../hooks/useSettings"; -export type SidebarStageBackdropVariant = "nightly" | "dev"; +export type SidebarStageBackdropVariant = + | { readonly kind: "nightly" } + | { readonly kind: "dev" } + | { readonly kind: "custom"; readonly image: string; readonly name: string }; export type EnvironmentIdentificationPillLabel = "Dev" | "Nightly"; +export const NIGHTLY_BACKDROP: SidebarStageBackdropVariant = { kind: "nightly" }; +export const DEV_BACKDROP: SidebarStageBackdropVariant = { kind: "dev" }; + +/** + * Resolve the artwork to draw from the account's choice. + * + * "auto" defers to the build channel the way upstream does; every other value + * is a deliberate pick and ignores the channel entirely, which is the point on + * a single-release build like MT Code. + */ +export function resolveSidebarArtwork(input: { + readonly selection: string; + readonly stageLabel: string; + readonly custom: ReadonlyArray<{ readonly id: string; readonly name: string; readonly image: string }>; + readonly enabled?: boolean; +}): SidebarStageBackdropVariant | null { + if (input.enabled === false) return null; + const selection = input.selection.trim(); + if (selection === "none") return null; + if (selection === "night") return NIGHTLY_BACKDROP; + if (selection === "day") return DEV_BACKDROP; + if (selection !== "auto" && selection.length > 0) { + const artwork = input.custom.find((entry) => entry.id === selection); + // A deleted artwork leaves the header bare rather than silently falling + // back to a scene the user did not choose. + return artwork ? { kind: "custom", image: artwork.image, name: artwork.name } : null; + } + return resolveSidebarStageBackdropVariant(input.stageLabel); +} + // A wide viewBox keeps the 96-unit art height at a fixed scale while sidebar resizing reveals // more horizontal canvas instead of zooming the scene. const STAGE_BACKDROP_VIEW_BOX = "0 0 8192 96"; @@ -18,15 +52,15 @@ export function resolveSidebarStageBackdropVariant( ): SidebarStageBackdropVariant | null { if (!enabled) return null; const normalized = stageLabel.trim().toLowerCase(); - if (normalized === "nightly") return "nightly"; - if (normalized === "dev") return "dev"; + if (normalized === "nightly") return NIGHTLY_BACKDROP; + if (normalized === "dev") return DEV_BACKDROP; return null; } export function resolveSidebarStageFocusRingOffsetClass( variant: SidebarStageBackdropVariant, ): string { - return variant === "nightly" + return variant.kind === "nightly" ? "focus-visible:ring-offset-(--stage-night-bottom)" : "focus-visible:ring-offset-(--stage-art-bottom)"; } @@ -52,7 +86,17 @@ export function useEnvironmentStageLabel(): string { } export function useSidebarStageBackdropVariant(enabled = true): SidebarStageBackdropVariant | null { - return resolveSidebarStageBackdropVariant(useEnvironmentStageLabel(), enabled); + const stageLabel = useEnvironmentStageLabel(); + const artwork = usePrimarySettings((settings) => ({ + selection: settings.sidebarArtwork, + custom: settings.customSidebarArtworks, + })); + return resolveSidebarArtwork({ + selection: artwork.selection, + stageLabel, + custom: artwork.custom, + enabled, + }); } /** Stage-channel header art; palettes mirror the per-channel app icons in `assets/`. */ @@ -68,11 +112,32 @@ export function SidebarStageBackdrop({ variant }: { variant: SidebarStageBackdro } export function StageBackdropArt({ variant }: { variant: SidebarStageBackdropVariant }) { - return variant === "nightly" ? : ; + if (variant.kind === "custom") return ; + return variant.kind === "nightly" ? : ; } export function StageBackdropButtonArt({ variant }: { variant: SidebarStageBackdropVariant }) { - return variant === "nightly" ? : ; + if (variant.kind === "custom") return ; + return variant.kind === "nightly" ? : ; +} + +/** + * A user's own artwork. Drawn as a cover-cropped band so any aspect ratio + * fills the header the way the built-in scenes do. + */ +function CustomArt({ + variant, +}: { + variant: Extract; +}) { + return ( +
+ ); } const NIGHTLY_STARS: ReadonlyArray<{ diff --git a/apps/web/src/components/settings/DesktopNotificationsSettings.tsx b/apps/web/src/components/settings/DesktopNotificationsSettings.tsx index dd3ff01d60ca..f159afcaf2cf 100644 --- a/apps/web/src/components/settings/DesktopNotificationsSettings.tsx +++ b/apps/web/src/components/settings/DesktopNotificationsSettings.tsx @@ -16,6 +16,8 @@ import { toastManager } from "../ui/toast.tsx"; import { SettingsRow, SettingsSection } from "./settingsLayout.tsx"; import { searchableSetting } from "./settingsSearch.ts"; +import { APP_DISPLAY_NAME } from "~/branding"; + const EVENT_OPTIONS: ReadonlyArray<{ readonly event: DesktopNotificationEvent; readonly title: string; @@ -158,14 +160,14 @@ function useDesktopNotificationSettingsModel() { }; const masterDescription = isElectron - ? "Notify me when T3 Code is in the background." + ? `Notify me when ${APP_DISPLAY_NAME} is in the background.` : browserPermission.permission === "denied" ? "Blocked in your browser settings." : browserPermission.permission === "unsupported" ? "Not supported by this browser." : browserPermission.permission === "default" - ? "Allow notifications when T3 Code is in the background." - : "Notify me when T3 Code is in the background."; + ? `Allow notifications when ${APP_DISPLAY_NAME} is in the background.` + : `Notify me when ${APP_DISPLAY_NAME} is in the background.`; return { settings, diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 38f700e23b55..282ab27156fb 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -19,6 +19,8 @@ import { } from "@t3tools/client-runtime/state/runtime"; import { DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE, + DEFAULT_SIDEBAR_ARTWORK_SELECTION, + MAX_CUSTOM_SIDEBAR_ARTWORK_BYTES, DEFAULT_UNIFIED_SETTINGS, type EnvironmentIdentificationMode, MAX_CODE_FONT_SIZE, @@ -1154,6 +1156,8 @@ export function AppearanceSettingsPanel() { } /> ) : null} + + @@ -1444,6 +1448,128 @@ const ADVANCED_TYPOGRAPHY_TARGET_IDS: ReadonlySet = new Set([ * and a settings-search jump to an override row flips Advanced on so the * target exists to scroll to. */ +/** + * Sidebar artwork: pick a built-in scene, one of your own, or nothing. + * + * Upstream only draws artwork on Dev/Nightly builds. MT Code ships a single + * release, so the artwork is a preference; custom pieces live in server + * settings, which is what makes them follow the account to every client + * attached to it rather than sitting in one browser's local storage. + */ +function SidebarArtworkRow() { + const settings = usePrimarySettings(); + const updateSettings = useUpdatePrimarySettings(); + const fileInputRef = useRef(null); + const [addError, setAddError] = useState(null); + const custom = settings.customSidebarArtworks; + const selection = settings.sidebarArtwork; + + const options = useMemo( + () => [ + { value: "auto", label: "Match the build" }, + { value: "none", label: "None" }, + { value: "night", label: "Night sky" }, + { value: "day", label: "Blueprint" }, + ...custom.map((artwork) => ({ value: artwork.id, label: artwork.name })), + ], + [custom], + ); + + const onPickFile = async (file: File) => { + setAddError(null); + if (file.size > MAX_CUSTOM_SIDEBAR_ARTWORK_BYTES * 0.7) { + // base64 inflates by ~4/3, so the on-disk limit is hit before the file one. + setAddError("That image is too large. Pick one under about 350 KB."); + return; + } + const image = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : null); + reader.onerror = () => resolve(null); + reader.readAsDataURL(file); + }); + if (image === null || !image.startsWith("data:image/")) { + setAddError("That file could not be read as an image."); + return; + } + const id = `art_${Math.random().toString(36).slice(2, 10)}`; + const name = file.name.replace(/\.[^.]+$/, "").slice(0, 60) || "Artwork"; + updateSettings({ + customSidebarArtworks: [...custom, { id, name, image, createdAt: new Date().toISOString() }], + sidebarArtwork: id, + }); + }; + + const removeArtwork = (id: string) => { + updateSettings({ + customSidebarArtworks: custom.filter((artwork) => artwork.id !== id), + ...(selection === id ? { sidebarArtwork: "auto" } : {}), + }); + }; + + return ( + updateSettings({ sidebarArtwork: DEFAULT_SIDEBAR_ARTWORK_SELECTION })} + /> + ) : null + } + control={ +
+ +
+ {custom.some((artwork) => artwork.id === selection) ? ( + + ) : null} + +
+ { + const file = event.target.files?.[0]; + event.target.value = ""; + if (file) void onPickFile(file); + }} + /> + {addError !== null ? ( + {addError} + ) : null} +
+ } + /> + ); +} + function TypographySection() { const [advanced, setAdvanced] = useLocalStorage( TYPOGRAPHY_ADVANCED_STORAGE_KEY, diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 64b6bf4027fc..3377aa73e1b2 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -212,6 +212,11 @@ export const SETTINGS_SEARCH_ITEMS = [ title: "Workspace tabs", to: "/settings/general", }, + { + id: "sidebar-artwork", + title: "Sidebar artwork", + to: "/settings/appearance", + }, { id: "keybindings", title: "Keybindings", diff --git a/native/t3-chrome-extension/background.js b/native/t3-chrome-extension/background.js index 61ade6b7e2a4..6123e426d32b 100644 --- a/native/t3-chrome-extension/background.js +++ b/native/t3-chrome-extension/background.js @@ -1,4 +1,4 @@ -// T3 Code desktop control — Chrome side. +// MT Code desktop control — Chrome side. // // The agent works only in tabs it created, collected into a labelled tab group, // so the user's own tabs are never touched and they can keep browsing while a @@ -11,7 +11,7 @@ // carries the originating request id. const HOST = "com.t3tools.t3code.desktop"; -const GROUP_TITLE = "T3 Code"; +const GROUP_TITLE = "MT Code"; const OWNED_STATE_KEY = "ownedState"; /** Tabs this extension owns, and the group holding them. */ @@ -634,7 +634,7 @@ async function navigate(tabId, url) { // // Toolbar icon = T3 logo (manifest icons/). Tab favicon = Computer Use badge // (icons/pointer-*.png), matching Settings → Agent cursor overlay — so a tab -// in the "T3 Code" group is visually distinct from the extension itself. +// in the "MT Code" group is visually distinct from the extension itself. // // An extension cannot set a tab's favicon directly, but it can replace the // page's icon link, which is what Chrome renders in the tab strip. Pages diff --git a/native/t3-chrome-extension/icons/icon-128.png b/native/t3-chrome-extension/icons/icon-128.png index 9f1e1895706f..f9a944eda5aa 100644 Binary files a/native/t3-chrome-extension/icons/icon-128.png and b/native/t3-chrome-extension/icons/icon-128.png differ diff --git a/native/t3-chrome-extension/icons/icon-16.png b/native/t3-chrome-extension/icons/icon-16.png index 841a2e479cbc..adf444cd1b8f 100644 Binary files a/native/t3-chrome-extension/icons/icon-16.png and b/native/t3-chrome-extension/icons/icon-16.png differ diff --git a/native/t3-chrome-extension/icons/icon-32.png b/native/t3-chrome-extension/icons/icon-32.png index 96b9d50107a8..31c7a4f39c64 100644 Binary files a/native/t3-chrome-extension/icons/icon-32.png and b/native/t3-chrome-extension/icons/icon-32.png differ diff --git a/native/t3-chrome-extension/icons/icon-48.png b/native/t3-chrome-extension/icons/icon-48.png index 4c243db6e469..0d33f86cbf43 100644 Binary files a/native/t3-chrome-extension/icons/icon-48.png and b/native/t3-chrome-extension/icons/icon-48.png differ diff --git a/native/t3-chrome-extension/install.ps1 b/native/t3-chrome-extension/install.ps1 index afe6bb9cc703..073421c6ccb3 100644 --- a/native/t3-chrome-extension/install.ps1 +++ b/native/t3-chrome-extension/install.ps1 @@ -40,7 +40,7 @@ $utf8NoBom = New-Object System.Text.UTF8Encoding $false $manifestPath = Join-Path $support "$HostName.json" $manifest = [ordered]@{ name = $HostName - description = 'T3 Code desktop control bridge' + description = 'MT Code desktop control bridge' path = $wrapper type = 'stdio' allowed_origins = @("chrome-extension://$ExtensionId/") diff --git a/native/t3-chrome-extension/install.sh b/native/t3-chrome-extension/install.sh index 2806e53f0245..1caaa28471ec 100755 --- a/native/t3-chrome-extension/install.sh +++ b/native/t3-chrome-extension/install.sh @@ -63,7 +63,7 @@ for profile in "$@"; do cat > "$dir/$HOST_NAME.json" <"], + "host_permissions": [ + "" + ], "background": { "service_worker": "background.js" }, "content_scripts": [ { - "matches": [""], - "js": ["wake.js"], + "matches": [ + "" + ], + "js": [ + "wake.js" + ], "run_at": "document_start", "all_frames": false } @@ -41,7 +47,9 @@ "icons/pointer-48.png", "icons/pointer-64.png" ], - "matches": [""] + "matches": [ + "" + ] } ] } diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 613cff012300..7c4c465a9278 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -114,6 +114,39 @@ export const TerminalFontSize = Schema.Int.check( export type TerminalFontSize = typeof TerminalFontSize.Type; export const DEFAULT_TERMINAL_FONT_SIZE: TerminalFontSize = 12; +/** + * Sidebar header artwork. + * + * Upstream ties the artwork to the Dev/Nightly channel; MT Code ships one + * release, so the artwork is a choice instead of a side effect of a build + * channel. "auto" keeps the upstream behaviour (art on Dev/Nightly, nothing + * on a release build); the named scenes and any custom artwork are explicit. + */ +export const SIDEBAR_ARTWORK_BUILT_IN_IDS = ["auto", "none", "night", "day"] as const; +export const SidebarArtworkSelection = TrimmedNonEmptyString.check( + Schema.isMaxLength(80), +); +export type SidebarArtworkSelection = typeof SidebarArtworkSelection.Type; +export const DEFAULT_SIDEBAR_ARTWORK_SELECTION = "auto"; + +/** Largest artwork a user may store, so settings stay a settings file. */ +export const MAX_CUSTOM_SIDEBAR_ARTWORK_BYTES = 512 * 1024; + +/** + * A user's own artwork. Stored on the server so it follows the account to + * every client attached to it - desktop, browser, phone - rather than living + * in one device's local storage. + */ +export const CustomSidebarArtwork = Schema.Struct({ + id: TrimmedNonEmptyString.check(Schema.isMaxLength(80)), + name: TrimmedNonEmptyString.check(Schema.isMaxLength(60)), + /** `data:` URL for an image (PNG, JPEG, WEBP, or SVG). */ + image: TrimmedNonEmptyString.check(Schema.isMaxLength(MAX_CUSTOM_SIDEBAR_ARTWORK_BYTES)), + /** ISO timestamp; a plain string so the settings schema stays service-free. */ + createdAt: TrimmedNonEmptyString.check(Schema.isMaxLength(40)), +}); +export type CustomSidebarArtwork = typeof CustomSidebarArtwork.Type; + export const EnvironmentIdentificationMode = Schema.Literals(["artwork", "pill", "none"]); export type EnvironmentIdentificationMode = typeof EnvironmentIdentificationMode.Type; export const DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE: EnvironmentIdentificationMode = "artwork"; @@ -763,6 +796,14 @@ export const ServerSettings = Schema.Struct({ environmentLabel: TrimmedString.check(Schema.isMaxLength(40)).pipe( Schema.withDecodingDefault(Effect.succeed("")), ), + /** Which sidebar artwork this account shows; see SidebarArtworkSelection. */ + sidebarArtwork: SidebarArtworkSelection.pipe( + Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_ARTWORK_SELECTION)), + ), + /** The account's own artwork, synced to every client on this server. */ + customSidebarArtworks: Schema.Array(CustomSidebarArtwork).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), // Legacy token-by-token assistant output. Deliberately a fresh key (was // `enableAssistantStreaming`): decoding drops the old key, so everyone, // including prior opt-ins, resets to the buffered default. @@ -1015,6 +1056,8 @@ const AntigravitySettingsPatch = Schema.Struct({ export const ServerSettingsPatch = Schema.Struct({ // Server settings environmentLabel: Schema.optionalKey(TrimmedString.check(Schema.isMaxLength(40))), + sidebarArtwork: Schema.optionalKey(SidebarArtworkSelection), + customSidebarArtworks: Schema.optionalKey(Schema.Array(CustomSidebarArtwork)), enableLegacyTokenStreaming: Schema.optionalKey(Schema.Boolean), enableProviderUpdateChecks: Schema.optionalKey(Schema.Boolean), enableAgentBrowserAccess: Schema.optionalKey(Schema.Boolean), diff --git a/scripts/personal-publish-web.sh b/scripts/personal-publish-web.sh index 76a68f5ac275..54339e4ba146 100755 --- a/scripts/personal-publish-web.sh +++ b/scripts/personal-publish-web.sh @@ -32,10 +32,18 @@ if [[ "$MUNIM_CONNECT_ACTIVE" != 1 ]]; then echo "building without Munim Clerk; T3 Connect will still be offered as an external option" fi -# Match the desktop munim distro: Nightly stage (sidebar artwork) with a plain -# "MT Code" shell name, no "(Alpha)"/"(Nightly)" suffix. -VITE_HOSTED_APP_URL="$HOSTED_URL" VITE_APP_BASE_NAME="MT Code" \ - VITE_APP_STAGE_LABEL="Nightly" VITE_APP_DISPLAY_NAME="MT Code" \ +# Same single version the desktop builds stamp, so About on the hosted app +# matches the installers instead of reporting the checked-in package version. +# shellcheck source=lib/personal-mt-version.sh +source "$REPO/scripts/lib/personal-mt-version.sh" +personal_mt_export_desktop_version +echo "web version $T3CODE_DESKTOP_VERSION" + +# Match the desktop munim distro: plain "MT Code", one release, no stage +# suffix. Sidebar artwork is a setting now, not a side effect of a channel. +APP_VERSION="$T3CODE_DESKTOP_VERSION" \ + VITE_HOSTED_APP_URL="$HOSTED_URL" VITE_APP_BASE_NAME="MT Code" \ + VITE_APP_DISPLAY_NAME="MT Code" \ vp run --filter @t3tools/web build node scripts/apply-web-brand-assets.ts munim apps/web/dist