Compare commits

..
Author SHA1 Message Date
EthanHealy01andGitHub fb28de4d5e Merge branch 'main' into dev/ChangeBrowserLabelToMatchWorktree 2026-07-09 21:03:23 +01:00
EthanHealy01 eb754d12c3 feat(dev): prefix browser tab title with worktree name in dev
When running task dev / dev:all / dev:saas / dev:portal from a worktree,
the frontend dev server injects the worktree folder basename (e.g. wt1)
as a build-time constant, and the app prefixes the browser tab title with
it so concurrent worktrees are distinguishable instead of all reading
"Stirling PDF".

Only the folder basename is exposed (never path/host/user), and only at
vite dev-serve time — production builds inject an empty string and the
feature compiles to a no-op. Desktop (tauri dev) is unaffected.
2026-07-09 14:19:33 +01:00
10 changed files with 76 additions and 27 deletions
+6
View File
@@ -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}}
@@ -7,15 +7,15 @@
* tool list unchanged.
*/
import { useMemo, type ReactNode } from "react";
import type { ReactNode } from "react";
/** Whether the right rail should host the Policies section. False in core. */
export function usePoliciesEnabled(): boolean {
return useMemo(() => false, []);
return false;
}
/** Whether the Policies list should appear for the current user. False in core. */
export function usePoliciesVisible(_enabled = false): boolean {
export function usePoliciesVisible(): boolean {
return false;
}
@@ -66,7 +66,7 @@ export default function RightSidebar() {
} = useToolWorkflow();
const policiesEnabled = usePoliciesEnabled();
const policiesVisible = usePoliciesVisible(policiesEnabled);
const policiesVisible = usePoliciesVisible();
const rawPolicyDetailActive = usePolicyDetailActive();
const fullscreenExpanded = useIsFullscreenExpanded();
const fullscreenGeometry = useToolPanelGeometry({
@@ -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,
});
}
}
+3
View File
@@ -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") {
@@ -90,10 +90,10 @@ export function usePoliciesEnabled(): boolean {
/**
* Whether the right rail should show the Policies section.
*/
export function usePoliciesVisible(enabled = POLICIES_ENABLED): boolean {
const pol = usePolicies(enabled);
export function usePoliciesVisible(): boolean {
const pol = usePolicies();
const { categories } = usePolicyCatalog();
if (!enabled) return false;
if (!POLICIES_ENABLED) return false;
return pol.canConfigure || categories.some((c) => !c.comingSoon);
}
@@ -18,6 +18,8 @@ if (typeof globalThis.crypto?.randomUUID !== "function") {
const api = vi.hoisted(() => ({
store: new Map<string, { id: string }>(),
seq: 0,
}));
vi.mock("@app/services/policyApi", () => ({
listPolicies: vi.fn(async () => [...api.store.values()]),
savePolicy: vi.fn(async (p: { id?: string }) => {
const id = p.id && p.id.length > 0 ? p.id : `be-${++api.seq}`;
@@ -33,15 +35,6 @@ const api = vi.hoisted(() => ({
runPolicyPipeline: vi.fn(),
getPolicyRun: vi.fn(),
}));
vi.mock("@app/services/policyApi", () => ({
listPolicies: api.listPolicies,
savePolicy: api.savePolicy,
getPolicy: api.getPolicy,
deletePolicy: api.deletePolicy,
runStoredPolicy: api.runStoredPolicy,
runPolicyPipeline: api.runPolicyPipeline,
getPolicyRun: api.getPolicyRun,
}));
import { usePolicies } from "@app/hooks/usePolicies";
@@ -72,7 +65,6 @@ const wizardResult = {
describe("usePolicies", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
api.store.clear();
api.seq = 0;
@@ -86,12 +78,6 @@ describe("usePolicies", () => {
expect(result.current.policies.security.configured).toBe(false);
});
it("skips the backend reconcile when disabled", async () => {
renderHook(() => usePolicies(false));
await act(async () => {});
expect(api.listPolicies).not.toHaveBeenCalled();
});
it("enabling a policy persists it to the backend + marks it configured", async () => {
const { result } = renderHook(() => usePolicies());
await act(async () => {
@@ -73,7 +73,7 @@ function toStoreRequest(
};
}
export function usePolicies(enabled = true) {
export function usePolicies() {
const [policies, setPolicies] = useState<PoliciesByCategory>(loadPolicies);
const { config, refetch: refetchAppConfig } = useAppConfig();
const { isTeamLeader } = useSaaSTeam();
@@ -88,7 +88,6 @@ export function usePolicies(enabled = true) {
// locally-cached folderId; retry with backoff since the backend may not be up yet.
// On recovery, also re-resolve app config in case its admin/team-leader flags settled false while down.
useEffect(() => {
if (!enabled) return;
let cancelled = false;
let attempt = 0;
let timer: ReturnType<typeof setTimeout> | undefined;
@@ -128,7 +127,7 @@ export function usePolicies(enabled = true) {
cancelled = true;
if (timer) clearTimeout(timer);
};
}, [enabled]);
}, []);
/**
* Enable a new policy from the wizard result: persist it to the backend (the
+7
View File
@@ -29,3 +29,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;
+10 -1
View File
@@ -182,7 +182,13 @@ const TSCONFIG_MAP: Record<BuildMode, string> = {
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)] : []),