From 880d9ced3aac314287ce5b67b75daa95665d6d66 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Wed, 18 Mar 2026 10:29:04 -0500 Subject: [PATCH] feat: auto-open HTML reports in default browser on manual export (#1164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When running /gsd export --html, the generated report now automatically opens in the user's default browser. Uses platform-specific commands (open/xdg-open/start). Only applies to manual exports — auto-mode milestone completion reports do not auto-open. --- src/resources/extensions/gsd/export.ts | 30 ++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/resources/extensions/gsd/export.ts b/src/resources/extensions/gsd/export.ts index 5a17f61c1..94a813113 100644 --- a/src/resources/extensions/gsd/export.ts +++ b/src/resources/extensions/gsd/export.ts @@ -4,6 +4,7 @@ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; import { writeFileSync, mkdirSync } from "node:fs"; import { join, basename } from "node:path"; +import { exec } from "node:child_process"; import { getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice, aggregateByModel, formatCost, formatTokenCount, loadLedgerFromDisk, @@ -12,6 +13,28 @@ import type { UnitMetrics } from "./metrics.js"; import { gsdRoot } from "./paths.js"; import { formatDuration, fileLink } from "../shared/mod.js"; +/** + * Open a file in the user's default browser. + * Uses platform-specific commands: `open` (macOS), `xdg-open` (Linux), `start` (Windows). + * Non-blocking, non-fatal — failures are silently ignored. + */ +export function openInBrowser(filePath: string): void { + const cmd = + process.platform === "darwin" ? "open" : + process.platform === "win32" ? "start" : + "xdg-open"; + + // On Windows, `start` needs an empty title argument when the path has spaces + const args = process.platform === "win32" + ? `"" "${filePath}"` + : `"${filePath}"`; + + exec(`${cmd} ${args}`, (err) => { + // Non-fatal — if the browser can't be opened, the file path is still shown + if (err) void err; + }); +} + /** * Write an export file directly, without requiring an ExtensionCommandContext. * Used by the visualizer overlay export tab. @@ -167,10 +190,12 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b paths.push(bn(outPath)); } + const indexPath = join(gsdRoot(basePath), "reports", "index.html"); ctx.ui.notify( - `Generated ${paths.length} report snapshot${paths.length !== 1 ? "s" : ""}:\n${paths.map(p => ` ${p}`).join("\n")}\nBrowse all reports: .gsd/reports/index.html`, + `Generated ${paths.length} report snapshot${paths.length !== 1 ? "s" : ""}:\n${paths.map(p => ` ${p}`).join("\n")}\nOpening reports index in browser...`, "success", ); + openInBrowser(indexPath); } else { // Single report for the active milestone (existing behavior) const doneSlices = data.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0); @@ -194,9 +219,10 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b phase: data.phase, }); ctx.ui.notify( - `HTML report saved: .gsd/reports/${bn(outPath)}\nBrowse all reports: .gsd/reports/index.html`, + `HTML report saved: .gsd/reports/${bn(outPath)}\nOpening in browser...`, "success", ); + openInBrowser(outPath); } } catch (err) { ctx.ui.notify(