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.
This commit is contained in:
Armin Ronacher 2026-10-01 14:42:02 +02:00
parent 409e808f58
commit f29ea3deb2
8 changed files with 64 additions and 18 deletions

View file

@ -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

View file

@ -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. |

View file

@ -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();

View file

@ -310,6 +310,7 @@ export {
type FullscreenExitOutput,
type ImageSettings,
type PackageSource,
type QuietStartup,
type RetrySettings,
SettingsManager,
type SettingsManagerCreateOptions,

View file

@ -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");

View file

@ -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);

View file

@ -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,

View file

@ -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: () => [],