From 0997b4945d35c4d78700f7e6b07cf477381515b6 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Sat, 21 Mar 2026 09:34:18 -0500 Subject: [PATCH] fix: remove duplicate TUI header rendered on session_start (#1663) --- src/resource-loader.ts | 10 ++++--- .../gsd/bootstrap/register-hooks.ts | 20 +++++++++++-- .../gsd/bootstrap/register-shortcuts.ts | 25 +--------------- .../search-the-web/native-search.ts | 16 +--------- src/tests/native-search.test.ts | 30 ++----------------- 5 files changed, 29 insertions(+), 72 deletions(-) diff --git a/src/resource-loader.ts b/src/resource-loader.ts index c421d40bd..f2b80a176 100644 --- a/src/resource-loader.ts +++ b/src/resource-loader.ts @@ -323,11 +323,13 @@ function pruneRemovedBundledExtensions( for (const prevFile of manifest.installedExtensionRootFiles) { removeIfStale(prevFile) } - } else { - // Fallback: explicitly remove known stale files from pre-manifest-tracking versions - // env-utils.js was moved from extensions/ root → gsd/ in v2.39.x (#1634) - removeIfStale('env-utils.js') } + + // Always remove known stale files regardless of manifest state. + // These were installed by pre-manifest versions so they may not appear in + // installedExtensionRootFiles even when a manifest exists. + // env-utils.js was moved from extensions/ root → gsd/ in v2.39.x (#1634) + removeIfStale('env-utils.js') } /** diff --git a/src/resources/extensions/gsd/bootstrap/register-hooks.ts b/src/resources/extensions/gsd/bootstrap/register-hooks.ts index dc2632fbd..2a31edeab 100644 --- a/src/resources/extensions/gsd/bootstrap/register-hooks.ts +++ b/src/resources/extensions/gsd/bootstrap/register-hooks.ts @@ -14,12 +14,28 @@ import { deriveState } from "../state.js"; import { getAutoDashboardData, isAutoActive, isAutoPaused, markToolEnd, markToolStart } from "../auto.js"; import { isParallelActive, shutdownParallel } from "../parallel-orchestrator.js"; import { saveActivityLog } from "../activity-log.js"; -import { maybeRenderGsdHeader } from "./register-shortcuts.js"; + +// Skip the welcome screen on the very first session_start — cli.ts already +// printed it before the TUI launched. Only re-print on /clear (subsequent sessions). +let isFirstSession = true; export function registerHooks(pi: ExtensionAPI): void { pi.on("session_start", async (_event, ctx) => { resetWriteGateState(); - maybeRenderGsdHeader(ctx); + if (isFirstSession) { + isFirstSession = false; + } else { + try { + const gsdBinPath = process.env.GSD_BIN_PATH; + if (gsdBinPath) { + const { dirname } = await import('node:path'); + const { printWelcomeScreen } = await import( + join(dirname(gsdBinPath), 'welcome-screen.js') + ) as { printWelcomeScreen: (opts: { version: string; modelName?: string; provider?: string }) => void }; + printWelcomeScreen({ version: process.env.GSD_VERSION || '0.0.0' }); + } + } catch { /* non-fatal */ } + } loadToolApiKeys(); try { const [{ getRemoteConfigStatus }, { getLatestPromptSummary }] = await Promise.all([ diff --git a/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts b/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts index c04f58cb7..ea94bc9dd 100644 --- a/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +++ b/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts @@ -2,20 +2,11 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { Key, Text } from "@gsd/pi-tui"; +import { Key } from "@gsd/pi-tui"; import { GSDDashboardOverlay } from "../dashboard-overlay.js"; import { shortcutDesc } from "../../shared/mod.js"; -export const GSD_LOGO_LINES = [ - " ██████╗ ███████╗██████╗ ", - " ██╔════╝ ██╔════╝██╔══██╗", - " ██║ ███╗███████╗██║ ██║", - " ██║ ██║╚════██║██║ ██║", - " ╚██████╔╝███████║██████╔╝", - " ╚═════╝ ╚══════╝╚═════╝ ", -]; - export function registerShortcuts(pi: ExtensionAPI): void { pi.registerShortcut(Key.ctrlAlt("g"), { description: shortcutDesc("Open GSD dashboard", "/gsd status"), @@ -39,17 +30,3 @@ export function registerShortcuts(pi: ExtensionAPI): void { }, }); } - -export function maybeRenderGsdHeader(ctx: { ui: any }): void { - try { - const theme = ctx.ui.theme; - const version = process.env.GSD_VERSION || "0.0.0"; - const logoText = GSD_LOGO_LINES.map((line) => theme.fg("accent", line)).join("\n"); - const titleLine = ` ${theme.bold("Get Shit Done")} ${theme.fg("dim", `v${version}`)}`; - const headerContent = `${logoText}\n${titleLine}`; - ctx.ui.setHeader((_ui: unknown, _theme: unknown) => new Text(headerContent, 1, 0)); - } catch { - // no TUI - } -} - diff --git a/src/resources/extensions/search-the-web/native-search.ts b/src/resources/extensions/search-the-web/native-search.ts index 46b355e00..a153f8cc3 100644 --- a/src/resources/extensions/search-the-web/native-search.ts +++ b/src/resources/extensions/search-the-web/native-search.ts @@ -216,23 +216,9 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): { getIsAnthropic: return payload; }); - // Basic startup diagnostics — provider-specific info comes from model_select - pi.on("session_start", async (_event: any, ctx: any) => { + pi.on("session_start", async (_event: any, _ctx: any) => { // Reset session-level search budget (#1309) sessionSearchCount = 0; - - const hasBrave = !!process.env.BRAVE_API_KEY; - const hasJina = !!process.env.JINA_API_KEY; - const hasAnswers = !!process.env.BRAVE_ANSWERS_KEY; - const hasTavily = !!process.env.TAVILY_API_KEY; - - const parts: string[] = ["Web search v4 loaded"]; - if (hasBrave) parts.push("Brave ✓"); - if (hasAnswers) parts.push("Answers ✓"); - if (hasJina) parts.push("Jina ✓"); - if (hasTavily) parts.push("Tavily ✓"); - - ctx.ui.notify(parts.join(" · "), "info"); }); return { getIsAnthropic: () => isAnthropicProvider }; diff --git a/src/tests/native-search.test.ts b/src/tests/native-search.test.ts index 9cabac87b..725c28f66 100644 --- a/src/tests/native-search.test.ts +++ b/src/tests/native-search.test.ts @@ -433,41 +433,17 @@ test("model_select shows warning for non-Anthropic without Brave key", async () } }); -test("session_start shows v4 loaded message", async () => { +test("session_start resets search count and shows no startup notification", async () => { const pi = createMockPI(); registerNativeSearchHooks(pi); await pi.fire("session_start", { type: "session_start" }); + // Tool status is now shown in the welcome screen bar layout — no notification on session_start const infoNotif = pi.notifications.find( (n) => n.level === "info" && n.message.includes("v4") ); - assert.ok(infoNotif, "Should have v4 info notification"); - assert.ok( - infoNotif!.message.startsWith("Web search v4 loaded"), - `Should start with 'Web search v4 loaded' — got: ${infoNotif!.message}` - ); -}); - -test("session_start shows Brave status when key present", async () => { - const originalKey = process.env.BRAVE_API_KEY; - process.env.BRAVE_API_KEY = "test-key"; - - try { - const pi = createMockPI(); - registerNativeSearchHooks(pi); - - await pi.fire("session_start", { type: "session_start" }); - - const info = pi.notifications.find((n) => n.level === "info"); - assert.ok(info!.message.includes("Brave"), "Should mention Brave in status"); - - const warning = pi.notifications.find((n) => n.level === "warning"); - assert.equal(warning, undefined, "Should NOT show warning when Brave key is present"); - } finally { - if (originalKey) process.env.BRAVE_API_KEY = originalKey; - else delete process.env.BRAVE_API_KEY; - } + assert.equal(infoNotif, undefined, "Should NOT emit a v4 startup notification (welcome screen handles this)"); }); test("BRAVE_TOOL_NAMES contains expected tool names", () => {