diff --git a/cli/src/__tests__/worktree.test.ts b/cli/src/__tests__/worktree.test.ts index dd832df4..117df23e 100644 --- a/cli/src/__tests__/worktree.test.ts +++ b/cli/src/__tests__/worktree.test.ts @@ -110,6 +110,7 @@ describe("worktree helpers", () => { const env = buildWorktreeEnvEntries(paths); expect(env.PAPERCLIP_HOME).toBe(path.resolve("/tmp/paperclip-worktrees")); expect(env.PAPERCLIP_INSTANCE_ID).toBe("feature-worktree-support"); + expect(env.PAPERCLIP_IN_WORKTREE).toBe("true"); expect(formatShellExports(env)).toContain("export PAPERCLIP_INSTANCE_ID='feature-worktree-support'"); }); diff --git a/cli/src/commands/worktree-lib.ts b/cli/src/commands/worktree-lib.ts index 63509371..4a0a3aeb 100644 --- a/cli/src/commands/worktree-lib.ts +++ b/cli/src/commands/worktree-lib.ts @@ -202,6 +202,7 @@ export function buildWorktreeEnvEntries(paths: WorktreeLocalPaths): Record + + + + + + + +`; + +describe("ui branding", () => { + it("detects worktree mode from PAPERCLIP_IN_WORKTREE", () => { + expect(isWorktreeUiBrandingEnabled({ PAPERCLIP_IN_WORKTREE: "true" })).toBe(true); + expect(isWorktreeUiBrandingEnabled({ PAPERCLIP_IN_WORKTREE: "1" })).toBe(true); + expect(isWorktreeUiBrandingEnabled({ PAPERCLIP_IN_WORKTREE: "false" })).toBe(false); + }); + + it("renders the worktree favicon asset set when enabled", () => { + const links = renderFaviconLinks(true); + expect(links).toContain("/worktree-favicon.ico"); + expect(links).toContain("/worktree-favicon.svg"); + expect(links).toContain("/worktree-favicon-32x32.png"); + expect(links).toContain("/worktree-favicon-16x16.png"); + }); + + it("rewrites the favicon block for worktree instances only", () => { + const branded = applyUiBranding(TEMPLATE, { PAPERCLIP_IN_WORKTREE: "true" }); + expect(branded).toContain("/worktree-favicon.svg"); + expect(branded).not.toContain('href="/favicon.svg"'); + + const defaultHtml = applyUiBranding(TEMPLATE, {}); + expect(defaultHtml).toContain('href="/favicon.svg"'); + expect(defaultHtml).not.toContain("/worktree-favicon.svg"); + }); +}); diff --git a/server/src/app.ts b/server/src/app.ts index 32b3e3bc..6871552a 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -24,6 +24,7 @@ import { sidebarBadgeRoutes } from "./routes/sidebar-badges.js"; import { llmRoutes } from "./routes/llms.js"; import { assetRoutes } from "./routes/assets.js"; import { accessRoutes } from "./routes/access.js"; +import { applyUiBranding } from "./ui-branding.js"; import type { BetterAuthSessionResult } from "./auth/better-auth.js"; type UiMode = "none" | "static" | "vite-dev"; @@ -135,7 +136,7 @@ export async function createApp( ]; const uiDist = candidates.find((p) => fs.existsSync(path.join(p, "index.html"))); if (uiDist) { - const indexHtml = fs.readFileSync(path.join(uiDist, "index.html"), "utf-8"); + const indexHtml = applyUiBranding(fs.readFileSync(path.join(uiDist, "index.html"), "utf-8")); app.use(express.static(uiDist)); app.get(/.*/, (_req, res) => { res.status(200).set("Content-Type", "text/html").end(indexHtml); @@ -168,7 +169,7 @@ export async function createApp( try { const templatePath = path.resolve(uiRoot, "index.html"); const template = fs.readFileSync(templatePath, "utf-8"); - const html = await vite.transformIndexHtml(req.originalUrl, template); + const html = applyUiBranding(await vite.transformIndexHtml(req.originalUrl, template)); res.status(200).set({ "Content-Type": "text/html" }).end(html); } catch (err) { next(err); diff --git a/server/src/ui-branding.ts b/server/src/ui-branding.ts new file mode 100644 index 00000000..bb6f3a33 --- /dev/null +++ b/server/src/ui-branding.ts @@ -0,0 +1,41 @@ +const FAVICON_BLOCK_START = ""; +const FAVICON_BLOCK_END = ""; + +const DEFAULT_FAVICON_LINKS = [ + '', + '', + '', + '', +].join("\n"); + +const WORKTREE_FAVICON_LINKS = [ + '', + '', + '', + '', +].join("\n"); + +function isTruthyEnvValue(value: string | undefined): boolean { + if (!value) return false; + const normalized = value.trim().toLowerCase(); + return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"; +} + +export function isWorktreeUiBrandingEnabled(env: NodeJS.ProcessEnv = process.env): boolean { + return isTruthyEnvValue(env.PAPERCLIP_IN_WORKTREE); +} + +export function renderFaviconLinks(worktree: boolean): string { + return worktree ? WORKTREE_FAVICON_LINKS : DEFAULT_FAVICON_LINKS; +} + +export function applyUiBranding(html: string, env: NodeJS.ProcessEnv = process.env): string { + const start = html.indexOf(FAVICON_BLOCK_START); + const end = html.indexOf(FAVICON_BLOCK_END); + if (start === -1 || end === -1 || end < start) return html; + + const before = html.slice(0, start + FAVICON_BLOCK_START.length); + const after = html.slice(end); + const links = renderFaviconLinks(isWorktreeUiBrandingEnabled(env)); + return `${before}\n${links}\n ${after}`; +} diff --git a/ui/index.html b/ui/index.html index 7c93cbf5..7994c0d2 100644 --- a/ui/index.html +++ b/ui/index.html @@ -8,10 +8,12 @@ Paperclip + +