From f29ea3deb298280b417892c6229ce478ad8c4d2f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 1 Oct 2026 14:42:02 +0200 Subject: [PATCH] feat(coding-agent): add header-only quietStartup mode quietStartup: "header" keeps the startup header with version and key hints but hides the model scope line and loaded-resource listing. --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/docs/settings.md | 2 +- .../coding-agent/src/core/settings-manager.ts | 11 +++++--- packages/coding-agent/src/index.ts | 1 + .../components/settings-selector.ts | 13 +++++----- .../src/modes/interactive/interactive-mode.ts | 26 ++++++++++++++----- .../test/interactive-mode-status.test.ts | 26 ++++++++++++++++++- .../5943-session-start-notify.test.ts | 2 ++ 8 files changed, 64 insertions(+), 18 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5700e14b4..4685b0a25 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Added an `oauth.authServerMetadataUrl` setting for MCP servers that advertise a wrong OAuth authorization server or none. Pi uses the configured metadata document instead of discovery ([#10172](https://github.com/earendil-works/pi/issues/10172)). +- Added `quietStartup: "header"`, which keeps the startup header with version and key hints but hides the model scope line and loaded-resource listing. ### Changed diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index 42c7871d7..788cff711 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -90,7 +90,7 @@ See [Compaction Reference](compaction.md) for trigger, summarization, and valida | Setting | Type | Default | Description | |---|---|---|---| | `theme` | string | `"system"` | Built-in or custom theme name. `system` derives colors from the terminal theme. | -| `quietStartup` | boolean | `false` | Hide the startup header. | +| `quietStartup` | boolean \| `"header"` | `false` | `true` hides the startup header and loaded-resource listing. `"header"` keeps the header (version and key hints) but hides the model scope line and loaded-resource listing. | | `tuiMode` | `"regular" \| "fullscreen"` | `"regular"` | Interactive terminal UI mode. | | `fullscreenExitOutput` | `"transcript" \| "resume-hint"` | `"transcript"` | Output printed when fullscreen mode exits. | | `fullscreenScrollbar` | `"auto" \| "always" \| "hidden"` | `"auto"` | Fullscreen transcript scrollbar behavior. | diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index aa3eae323..7dd11412d 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -108,6 +108,8 @@ export interface CodemodeSettings { } export type DefaultProjectTrust = "ask" | "always" | "never"; +/** true hides all startup output, "header" keeps only the startup header. */ +export type QuietStartup = boolean | "header"; export type TransportSetting = Transport; @@ -145,7 +147,7 @@ export interface Settings { showCacheMissNotices?: boolean; // default: false - show cache cost and provider recovery notices externalEditor?: string; // Command for Ctrl+G external editor; takes precedence over VISUAL/EDITOR shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows); supports leading ~ expansion - quietStartup?: boolean; + quietStartup?: QuietStartup; // default: false defaultProjectTrust?: DefaultProjectTrust; // default: "ask"; global setting only shellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., "shopt -s expand_aliases" for alias support) npmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., ["mise", "exec", "node@20", "--", "npm"]) @@ -1084,11 +1086,12 @@ export class SettingsManager { this.save(); } - getQuietStartup(): boolean { - return this.settings.quietStartup ?? false; + getQuietStartup(): QuietStartup { + const value = this.settings.quietStartup; + return value === true || value === "header" ? value : false; } - setQuietStartup(quiet: boolean): void { + setQuietStartup(quiet: QuietStartup): void { this.globalSettings.quietStartup = quiet; this.markModified("quietStartup"); this.save(); diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 9b9f273f2..9e2d993e7 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -310,6 +310,7 @@ export { type FullscreenExitOutput, type ImageSettings, type PackageSource, + type QuietStartup, type RetrySettings, SettingsManager, type SettingsManagerCreateOptions, diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index dc735f94b..7093e72ab 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -19,6 +19,7 @@ import { type DefaultProjectTrust, type FullscreenExitOutput, type MermaidRenderingMode, + type QuietStartup, type TuiMode, type WarningSettings, } from "../../../core/settings-manager.ts"; @@ -87,7 +88,7 @@ export interface SettingsConfig { editorPaddingX: number; outputPad: 0 | 1; autocompleteMaxVisible: number; - quietStartup: boolean; + quietStartup: QuietStartup; defaultProjectTrust: DefaultProjectTrust; clearOnShrink: boolean; showTerminalProgress: boolean; @@ -126,7 +127,7 @@ export interface SettingsCallbacks { onEditorPaddingXChange: (padding: number) => void; onOutputPadChange: (padding: 0 | 1) => void; onAutocompleteMaxVisibleChange: (maxVisible: number) => void; - onQuietStartupChange: (enabled: boolean) => void; + onQuietStartupChange: (quiet: QuietStartup) => void; onDefaultProjectTrustChange: (defaultProjectTrust: DefaultProjectTrust) => void; onClearOnShrinkChange: (enabled: boolean) => void; onShowTerminalProgressChange: (enabled: boolean) => void; @@ -553,9 +554,9 @@ export class SettingsSelectorComponent extends Container { { id: "quiet-startup", label: "Quiet startup", - description: "Disable verbose printing at startup", - currentValue: config.quietStartup ? "true" : "false", - values: ["true", "false"], + description: "Disable verbose printing at startup (header: keep only the startup header)", + currentValue: String(config.quietStartup), + values: ["true", "header", "false"], }, { id: "install-telemetry", @@ -920,7 +921,7 @@ export class SettingsSelectorComponent extends Container { callbacks.onCollapseChangelogChange(newValue === "true"); break; case "quiet-startup": - callbacks.onQuietStartupChange(newValue === "true"); + callbacks.onQuietStartupChange(newValue === "header" ? "header" : newValue === "true"); break; case "install-telemetry": callbacks.onEnableInstallTelemetryChange(newValue === "true"); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 7f9c21d0f..415098dbb 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -926,7 +926,7 @@ export class InteractiveMode { // Load changelog (only show new entries, skip for resumed sessions) this.changelogMarkdown = this.getChangelogForDisplay(); - if (this.session.scopedModels.length > 0 && (this.options.verbose || !this.settingsManager.getQuietStartup())) { + if (this.session.scopedModels.length > 0 && this.shouldShowStartupDetails()) { const modelList = this.session.scopedModels .map((sm) => { const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : ""; @@ -982,7 +982,8 @@ export class InteractiveMode { await this.themeController.waitForTerminalColors(); // Add header with keybindings from config (unless silenced) - if (this.options.verbose || !this.settingsManager.getQuietStartup()) { + if (this.shouldShowStartupHeader()) { + const showDetails = this.shouldShowStartupDetails(); // Built on demand so the header follows theme changes. The logo's first line carries the version, // its second line the first line of key hints. const withLogo = (hints: string) => { @@ -1027,7 +1028,10 @@ export class InteractiveMode { hint("app.tools.expand", "more"), ].join(theme.fg("muted", " ยท ")); const compactOnboarding = () => - theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`); + theme.fg( + "dim", + `Press ${keyText("app.tools.expand")} to show full startup help${showDetails ? " and loaded resources" : ""}.`, + ); const onboarding = () => theme.fg("dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`); const header = new BuiltInHeader( @@ -1388,6 +1392,16 @@ export class InteractiveMode { return this.options.verbose || this.toolOutputExpanded; } + /** Startup header (logo, version, key hints). Hidden only by quietStartup: true. */ + private shouldShowStartupHeader(): boolean { + return this.options.verbose === true || this.settingsManager.getQuietStartup() !== true; + } + + /** Startup details (model scope, loaded resources). Hidden by quietStartup: true or "header". */ + private shouldShowStartupDetails(): boolean { + return this.options.verbose === true || this.settingsManager.getQuietStartup() === false; + } + /** * Get a short path relative to the package root for display. */ @@ -1736,7 +1750,7 @@ export class InteractiveMode { // Resource rendering is idempotent; chat clears no longer clear this separate container. this.loadedResourcesContainer.clear(); - const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup(); + const showListing = options?.force || this.shouldShowStartupDetails(); const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true; if (!showListing && !showDiagnostics) { return; @@ -4937,8 +4951,8 @@ export class InteractiveMode { onEnableInstallTelemetryChange: (enabled) => { this.settingsManager.setEnableInstallTelemetry(enabled); }, - onQuietStartupChange: (enabled) => { - this.settingsManager.setQuietStartup(enabled); + onQuietStartupChange: (quiet) => { + this.settingsManager.setQuietStartup(quiet); }, onDefaultProjectTrustChange: (defaultProjectTrust) => { this.settingsManager.setDefaultProjectTrust(defaultProjectTrust); diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index a4aaa48c0..797c51165 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -6,6 +6,7 @@ import { type Component, Container, type Focusable, type TUI } from "../../tui/s import { TuiMainScreen } from "../../tui/src/tui-main-screen.ts"; import { VirtualTerminal } from "../../tui/test/virtual-terminal.ts"; import type { AutocompleteProviderFactory } from "../src/core/extensions/types.ts"; +import type { QuietStartup } from "../src/core/settings-manager.ts"; import type { SourceInfo } from "../src/core/source-info.ts"; import type { AuthSelectorProvider } from "../src/modes/interactive/components/oauth-selector.ts"; import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts"; @@ -513,7 +514,7 @@ describe("InteractiveMode.showLoadedResources", () => { }); function createShowLoadedResourcesThis(options: { - quietStartup: boolean; + quietStartup: QuietStartup; verbose?: boolean; toolOutputExpanded?: boolean; cwd?: string; @@ -561,6 +562,7 @@ describe("InteractiveMode.showLoadedResources", () => { (InteractiveMode as any).prototype.formatExtensionDisplayPath.call(fakeThis, p), formatContextPath: (p: string) => (InteractiveMode as any).prototype.formatContextPath.call(fakeThis, p), getStartupExpansionState: () => (InteractiveMode as any).prototype.getStartupExpansionState.call(fakeThis), + shouldShowStartupDetails: () => (InteractiveMode as any).prototype.shouldShowStartupDetails.call(fakeThis), buildScopeGroups: () => [], formatScopeGroups: () => "resource-list", isPackageSource: (sourceInfo?: SourceInfo) => @@ -1232,6 +1234,28 @@ describe("InteractiveMode.showLoadedResources", () => { expect(fakeThis.loadedResourcesContainer.children).toHaveLength(0); }); + test("hides resource listing but keeps the startup header with header-only quiet startup", () => { + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: "header", + skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }], + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + expect(fakeThis.loadedResourcesContainer.children).toHaveLength(0); + expect((InteractiveMode as any).prototype.shouldShowStartupHeader.call(fakeThis)).toBe(true); + expect((InteractiveMode as any).prototype.shouldShowStartupDetails.call(fakeThis)).toBe(false); + }); + + test("hides the startup header with full quiet startup unless verbose", () => { + const quiet = createShowLoadedResourcesThis({ quietStartup: true }); + expect((InteractiveMode as any).prototype.shouldShowStartupHeader.call(quiet)).toBe(false); + const verbose = createShowLoadedResourcesThis({ quietStartup: "header", verbose: true }); + expect((InteractiveMode as any).prototype.shouldShowStartupDetails.call(verbose)).toBe(true); + }); + test("still shows diagnostics on quiet startup when requested", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: true, diff --git a/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts b/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts index 913ee4e62..c73e90216 100644 --- a/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts +++ b/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts @@ -32,6 +32,7 @@ type LoadedResourcesContext = { }; }; getStartupExpansionState: () => boolean; + shouldShowStartupDetails: () => boolean; formatDisplayPath: (resourcePath: string) => string; formatContextPath: (resourcePath: string) => string; getBuiltInCommandConflictDiagnostics: (extensionRunner: LoadedResourcesContext["session"]["extensionRunner"]) => []; @@ -208,6 +209,7 @@ function createLoadedResourcesContext(): LoadedResourcesContext { }, }, getStartupExpansionState: () => false, + shouldShowStartupDetails: () => true, formatDisplayPath: (resourcePath) => resourcePath, formatContextPath: (resourcePath) => resourcePath.replace("/repo/", ""), getBuiltInCommandConflictDiagnostics: () => [],