diff --git a/.taskfiles/frontend.yml b/.taskfiles/frontend.yml index 4f87226f93..b402df7d1b 100644 --- a/.taskfiles/frontend.yml +++ b/.taskfiles/frontend.yml @@ -80,6 +80,12 @@ tasks: OPEN: '{{.OPEN | default ""}}' env: BACKEND_URL: '{{.BACKEND_URL}}' + # Dev-only browser-tab label so concurrent worktrees are distinguishable. + # Only the worktree folder basename (e.g. "wt1") is exposed — never the + # full path, hostname, or user. Consumed at dev-serve time by vite.config + # and dropped from production builds. + STIRLING_DEV_LABEL: + sh: basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" cmds: - npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}} diff --git a/frontend/editor/src/core/components/tools/ToolPicker.tsx b/frontend/editor/src/core/components/tools/ToolPicker.tsx index 27b0caf7d5..779e945b8c 100644 --- a/frontend/editor/src/core/components/tools/ToolPicker.tsx +++ b/frontend/editor/src/core/components/tools/ToolPicker.tsx @@ -197,7 +197,7 @@ const ToolPicker = ({ ) : ( <> {/* All-tools view: favourites + recommended + all subcategories. */} - + {favoriteToolItems.length > 0 && (
diff --git a/frontend/editor/src/core/utils/applyDevWorktreeLabel.ts b/frontend/editor/src/core/utils/applyDevWorktreeLabel.ts new file mode 100644 index 0000000000..f1c0164e27 --- /dev/null +++ b/frontend/editor/src/core/utils/applyDevWorktreeLabel.ts @@ -0,0 +1,39 @@ +/** + * Prefixes the browser tab title with the current worktree name during local + * development so multiple concurrently-running worktrees (e.g. wt1, wt2, spdf1) + * are distinguishable at a glance instead of all showing "Stirling PDF". + * + * The label is injected as a build-time constant by vite.config, sourced from + * the top-level dev tasks. It is an empty string in production builds, so this + * whole feature compiles away to a no-op outside `vite` dev-serve. + */ + +const LABEL = + typeof __DEV_WORKTREE_LABEL__ === "string" ? __DEV_WORKTREE_LABEL__ : ""; + +export function applyDevWorktreeLabel(): void { + if (!LABEL || typeof document === "undefined") { + return; + } + + const prefix = `[${LABEL}] `; + + const ensurePrefixed = () => { + if (!document.title.startsWith(prefix)) { + // The app rewrites document.title on route/tool changes; re-apply the + // prefix on top of whatever the app just set. + document.title = prefix + document.title; + } + }; + + ensurePrefixed(); + + const titleEl = document.querySelector("title"); + if (titleEl) { + new MutationObserver(ensurePrefixed).observe(titleEl, { + childList: true, + characterData: true, + subtree: true, + }); + } +} diff --git a/frontend/editor/src/index.tsx b/frontend/editor/src/index.tsx index d776918bd1..6ee0f6df51 100644 --- a/frontend/editor/src/index.tsx +++ b/frontend/editor/src/index.tsx @@ -14,9 +14,12 @@ import { BrowserRouter } from "react-router-dom"; import App from "@app/App"; import "@app/i18n"; // Initialize i18next import { BASE_PATH } from "@app/constants/app"; +import { applyDevWorktreeLabel } from "@app/utils/applyDevWorktreeLabel"; import { startEagerWasmCompilation } from "@app/services/wasmPrecompiler"; +applyDevWorktreeLabel(); + if (typeof window !== "undefined") { const scheduleCompilation = () => { if (typeof requestIdleCallback === "function") { diff --git a/frontend/editor/vite-env.d.ts b/frontend/editor/vite-env.d.ts index e9fb24e496..a410ba3bc7 100644 --- a/frontend/editor/vite-env.d.ts +++ b/frontend/editor/vite-env.d.ts @@ -32,3 +32,10 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv; } + +/** + * Dev-only worktree folder basename injected by vite.config at dev-serve time + * (empty string in production builds). Used to prefix the browser tab title so + * concurrent worktrees are distinguishable. + */ +declare const __DEV_WORKTREE_LABEL__: string; diff --git a/frontend/editor/vite.config.ts b/frontend/editor/vite.config.ts index 059f98332e..b5733bfe02 100644 --- a/frontend/editor/vite.config.ts +++ b/frontend/editor/vite.config.ts @@ -182,7 +182,13 @@ const TSCONFIG_MAP: Record = { prototypes: "./tsconfig.prototypes.vite.json", }; -export default defineConfig(async ({ mode }) => { +export default defineConfig(async ({ mode, command }) => { + // Dev-only browser-tab label (worktree folder basename) surfaced by the + // top-level dev tasks so concurrent worktrees have distinguishable tabs. + // Only injected during `vite` (dev serve) — never baked into a production + // build — and carries only the folder name, no path/host/user info. + const devWorktreeLabel = + command === "serve" ? (process.env.STIRLING_DEV_LABEL ?? "") : ""; // Load env files relative to this config (frontend/editor/), regardless of // where the build was invoked from. The previous `process.cwd()` worked when // this file lived at frontend/, but after the editor was moved under @@ -250,6 +256,9 @@ export default defineConfig(async ({ mode }) => { }; return { + define: { + __DEV_WORKTREE_LABEL__: JSON.stringify(devWorktreeLabel), + }, plugins: [ react(), ...(runSubpath ? [subpathBareRedirectPlugin(runSubpath)] : []),