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
7 changed files with 65 additions and 151 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}}
-11
View File
@@ -2,7 +2,6 @@ use tauri::{AppHandle, Emitter, Manager, RunEvent, WindowEvent};
mod utils;
pub mod commands;
mod menu;
mod state;
use commands::{
@@ -75,12 +74,6 @@ fn parse_launch_files(args: &[String]) -> Vec<String> {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.menu(menu::build_app_menu)
.on_menu_event(|app, event| {
if event.id() == menu::GRACEFUL_QUIT_MENU_ID {
menu::request_graceful_quit(app);
}
})
.plugin(
tauri_plugin_log::Builder::new()
.level(log::LevelFilter::Info)
@@ -216,10 +209,6 @@ pub fn run() {
// Use Tauri's built-in cleanup
app_handle.cleanup_before_exit();
}
RunEvent::Exit => {
add_log("🔄 App exiting, ensuring backend cleanup...".to_string());
cleanup_backend();
}
RunEvent::WindowEvent { event: WindowEvent::CloseRequested {.. }, .. } => {
add_log("🔄 Window close requested (will cleanup on actual exit)...".to_string());
// Don't cleanup here - let JavaScript handler prevent close if needed
-139
View File
@@ -1,139 +0,0 @@
use tauri::menu::Menu;
use tauri::{AppHandle, Manager, Runtime};
#[cfg(target_os = "macos")]
use tauri::menu::{
AboutMetadata, MenuItem, PredefinedMenuItem, Submenu, HELP_SUBMENU_ID, WINDOW_SUBMENU_ID,
};
use crate::utils::add_log;
// Id of our custom Quit item. macOS routes Cmd+Q / the app-menu Quit through
// [NSApp terminate:], which fires RunEvent::Exit (not ExitRequested) and never
// asks windows to close - so the frontend's unsaved-changes guard is bypassed.
// We intercept the shortcut with this item and route it through each window's
// close flow instead.
pub const GRACEFUL_QUIT_MENU_ID: &str = "graceful-quit";
// macOS: mirror Tauri's default menu (Menu::default) but replace the standard
// Quit item with a custom one so Cmd+Q goes through request_graceful_quit().
#[cfg(target_os = "macos")]
pub fn build_app_menu<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
let pkg = app.package_info();
let config = app.config();
let about = AboutMetadata {
name: Some(pkg.name.clone()),
version: Some(pkg.version.to_string()),
copyright: config.bundle.copyright.clone(),
authors: config.bundle.publisher.clone().map(|p| vec![p]),
..Default::default()
};
let quit = MenuItem::with_id(
app,
GRACEFUL_QUIT_MENU_ID,
format!("Quit {}", pkg.name),
true,
Some("CmdOrCtrl+Q"),
)?;
let app_menu = Submenu::with_items(
app,
pkg.name.clone(),
true,
&[
&PredefinedMenuItem::about(app, None, Some(about))?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::services(app, None)?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::hide(app, None)?,
&PredefinedMenuItem::hide_others(app, None)?,
&PredefinedMenuItem::separator(app)?,
&quit,
],
)?;
let file_menu = Submenu::with_items(
app,
"File",
true,
&[&PredefinedMenuItem::close_window(app, None)?],
)?;
let edit_menu = Submenu::with_items(
app,
"Edit",
true,
&[
&PredefinedMenuItem::undo(app, None)?,
&PredefinedMenuItem::redo(app, None)?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::cut(app, None)?,
&PredefinedMenuItem::copy(app, None)?,
&PredefinedMenuItem::paste(app, None)?,
&PredefinedMenuItem::select_all(app, None)?,
],
)?;
let view_menu = Submenu::with_items(
app,
"View",
true,
&[&PredefinedMenuItem::fullscreen(app, None)?],
)?;
let window_menu = Submenu::with_id_and_items(
app,
WINDOW_SUBMENU_ID,
"Window",
true,
&[
&PredefinedMenuItem::minimize(app, None)?,
&PredefinedMenuItem::maximize(app, None)?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::close_window(app, None)?,
],
)?;
let help_menu = Submenu::with_id_and_items(app, HELP_SUBMENU_ID, "Help", true, &[])?;
Menu::with_items(
app,
&[
&app_menu,
&file_menu,
&edit_menu,
&view_menu,
&window_menu,
&help_menu,
],
)
}
// Other platforms keep Tauri's default menu unchanged; the RunEvent::Exit
// cleanup path in lib.rs already guards against orphaned backends there.
#[cfg(not(target_os = "macos"))]
pub fn build_app_menu<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
Menu::default(app)
}
// Route a quit request through each window's close flow so the frontend's
// unsaved-changes guard (useExitWarning) runs. Closing the last window causes
// the runtime to emit RunEvent::ExitRequested, which kills the bundled backend.
// If somehow no windows exist, exit directly.
pub fn request_graceful_quit<R: Runtime>(app: &AppHandle<R>) {
let windows = app.webview_windows();
if windows.is_empty() {
add_log("Quit requested with no open windows; exiting.".to_string());
app.exit(0);
return;
}
add_log(format!(
"Quit requested; closing {} window(s) via close flow",
windows.len()
));
for (_, window) in windows {
let _ = window.close();
}
}
@@ -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") {
+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)] : []),