From 5957d5c2b61996f10de239424c6e4e90ade687b9 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sun, 19 Apr 2026 07:59:36 +0200 Subject: [PATCH] sf-tui + sf-permissions: gate footer-indicator side-effects on ctx.hasUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three TUI-only decorations were running their full session-lifecycle handlers even in headless mode, where there is no footer to render into. Most visibly, the emoji extension's AI auto-assign path made a real LLM call to pick a 🚀/✨/🎯 that nothing would ever see. - sf-tui/emoji.ts: session_start and agent_start handlers early-return when !ctx.hasUI. Commands stay registered so /emoji still works if someone runs it, but the lifecycle work (state loading, AI emoji selection, setStatus emission) is skipped. - sf-tui/color-band.ts: session_start and session_switch handlers early-return when !ctx.hasUI. Avoids unnecessary state-file writes and resize-listener attachment in headless runs. - sf-permissions/index.ts:setLevel: guards the setStatus("authority", …) call behind ctx.hasUI. The existing session_start path was already gated — this closes the permission-change code path. Headless stderr was already filtering these keys, so the user-visible output is unchanged. This eliminates the underlying RPC traffic and — more importantly — stops spending LLM tokens on decorative emoji selection in headless runs. Co-Authored-By: Claude Sonnet 4.6 --- src/resources/extensions/sf-permissions/index.ts | 4 +++- src/resources/extensions/sf-tui/color-band.ts | 5 +++++ src/resources/extensions/sf-tui/emoji.ts | 13 +++++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/resources/extensions/sf-permissions/index.ts b/src/resources/extensions/sf-permissions/index.ts index 2adab3dc1..5ba6083f3 100644 --- a/src/resources/extensions/sf-permissions/index.ts +++ b/src/resources/extensions/sf-permissions/index.ts @@ -205,7 +205,9 @@ function setLevel( if (saveGlobally) { saveGlobalPermission(level); } - if (ctx.ui?.setStatus) { + // Only emit the footer indicator when there's a real TUI to render into. + // In headless mode the "authority" badge has no consumer. + if (ctx.hasUI && ctx.ui?.setStatus) { ctx.ui.setStatus("authority", getStatusText(level)); } } diff --git a/src/resources/extensions/sf-tui/color-band.ts b/src/resources/extensions/sf-tui/color-band.ts index af8355b88..b6cd9f752 100644 --- a/src/resources/extensions/sf-tui/color-band.ts +++ b/src/resources/extensions/sf-tui/color-band.ts @@ -91,12 +91,17 @@ export function registerSessionColor(pi: ExtensionAPI): void { registerCommands(pi, state); + // Gate the session-lifecycle work on having a real TUI. The color band is + // pure footer decoration — nothing to render into in headless mode, so + // skip state-file writes and resize listeners entirely. pi.on("session_start", async (_, ctx) => { + if (!ctx.hasUI) return; currentCtx = ctx; initSession(ctx, state, setupResizeListener); }); pi.on("session_switch", async (event, ctx) => { + if (!ctx.hasUI) return; if (event.reason === "new") { currentCtx = ctx; initSession(ctx, state, setupResizeListener); diff --git a/src/resources/extensions/sf-tui/emoji.ts b/src/resources/extensions/sf-tui/emoji.ts index c0e016385..aadd887ce 100644 --- a/src/resources/extensions/sf-tui/emoji.ts +++ b/src/resources/extensions/sf-tui/emoji.ts @@ -78,8 +78,17 @@ export function registerSessionEmoji(pi: ExtensionAPI): void { registerCommands(pi, state); - pi.on("session_start", (_, ctx) => initSession(ctx, pi, state)); - pi.on("agent_start", (_, ctx) => handleAgentStart(ctx, pi, state)); + // Gate the session-lifecycle work on having a real TUI. Headless mode + // (sf headless auto, --print, CI) has no footer to render into, and the + // AI auto-assign path would spend tokens choosing an emoji nothing sees. + pi.on("session_start", (_, ctx) => { + if (!ctx.hasUI) return; + return initSession(ctx, pi, state); + }); + pi.on("agent_start", (_, ctx) => { + if (!ctx.hasUI) return; + return handleAgentStart(ctx, pi, state); + }); } // ─────────────────────────────────────────────────────────────────────────────