feat(cli): add configurable banner tagline mode

This commit is contained in:
Peter Steinberger
2026-03-03 00:31:42 +00:00
parent f6233cfa5c
commit 1b5ac8b0b1
15 changed files with 206 additions and 4 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
- Hooks/message lifecycle: add internal hook events `message:transcribed` and `message:preprocessed`, plus richer outbound `message:sent` context (`isGroup`, `groupId`) for group-conversation correlation and post-transcription automations. (#9859) Thanks @Drickon.
- Telegram/Streaming defaults: default `channels.telegram.streaming` to `partial` (from `off`) so new Telegram setups get live preview streaming out of the box, with runtime fallback to message-edit preview when native drafts are unavailable.
- CLI/Config validation: add `openclaw config validate` (with `--json`) to validate config files before gateway startup, and include detailed invalid-key paths in startup invalid-config errors. (#31220) thanks @Sid-Qin.
- CLI/Banner taglines: add `cli.banner.taglineMode` (`random` | `default` | `off`) to control funny tagline behavior in startup output, with docs + FAQ guidance and regression tests for config override behavior.
- Tools/Diffs: add PDF file output support and rendering quality customization controls (`fileQuality`, `fileScale`, `fileMaxWidth`) for generated diff artifacts, and document PDF as the preferred option when messaging channels compress images. (#31342) Thanks @gumadeiras.
- README/Contributors: rank contributor avatars by composite score (commits + merged PRs + code LOC), excluding docs-only LOC to prevent bulk-generated files from inflating rankings. (#23970) Thanks @tyler6204.

View File

@@ -2731,6 +2731,26 @@ Notes:
---
## CLI
```json5
{
cli: {
banner: {
taglineMode: "off", // random | default | off
},
},
}
```
- `cli.banner.taglineMode` controls banner tagline style:
- `"random"` (default): rotating funny/seasonal taglines.
- `"default"`: fixed neutral tagline (`All your chats, one OpenClaw.`).
- `"off"`: no tagline text (banner title/version still shown).
- To hide the entire banner (not just taglines), set env `OPENCLAW_HIDE_BANNER=1`.
---
## Wizard
Metadata written by CLI wizards (`onboard`, `configure`, `doctor`):

View File

@@ -101,6 +101,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
- [I set `gateway.bind: "lan"` (or `"tailnet"`) and now nothing listens / the UI says unauthorized](#i-set-gatewaybind-lan-or-tailnet-and-now-nothing-listens-the-ui-says-unauthorized)
- [Why do I need a token on localhost now?](#why-do-i-need-a-token-on-localhost-now)
- [Do I have to restart after changing config?](#do-i-have-to-restart-after-changing-config)
- [How do I disable funny CLI taglines?](#how-do-i-disable-funny-cli-taglines)
- [How do I enable web search (and web fetch)?](#how-do-i-enable-web-search-and-web-fetch)
- [config.apply wiped my config. How do I recover and avoid this?](#configapply-wiped-my-config-how-do-i-recover-and-avoid-this)
- [How do I run a central Gateway with specialized workers across devices?](#how-do-i-run-a-central-gateway-with-specialized-workers-across-devices)
@@ -1466,6 +1467,25 @@ The Gateway watches the config and supports hot-reload:
- `gateway.reload.mode: "hybrid"` (default): hot-apply safe changes, restart for critical ones
- `hot`, `restart`, `off` are also supported
### How do I disable funny CLI taglines
Set `cli.banner.taglineMode` in config:
```json5
{
cli: {
banner: {
taglineMode: "off", // random | default | off
},
},
}
```
- `off`: hides tagline text but keeps the banner title/version line.
- `default`: uses `All your chats, one OpenClaw.` every time.
- `random`: rotating funny/seasonal taglines (default behavior).
- If you want no banner at all, set env `OPENCLAW_HIDE_BANNER=1`.
### How do I enable web search and web fetch
`web_fetch` works without an API key. `web_search` requires a Brave Search API

60
src/cli/banner.test.ts Normal file
View File

@@ -0,0 +1,60 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const loadConfigMock = vi.fn();
vi.mock("../config/config.js", () => ({
loadConfig: loadConfigMock,
}));
let formatCliBannerLine: typeof import("./banner.js").formatCliBannerLine;
beforeAll(async () => {
({ formatCliBannerLine } = await import("./banner.js"));
});
beforeEach(() => {
loadConfigMock.mockReset();
loadConfigMock.mockReturnValue({});
});
describe("formatCliBannerLine", () => {
it("hides tagline text when cli.banner.taglineMode is off", () => {
loadConfigMock.mockReturnValue({
cli: { banner: { taglineMode: "off" } },
});
const line = formatCliBannerLine("2026.3.3", {
commit: "abc1234",
richTty: false,
});
expect(line).toBe("🦞 OpenClaw 2026.3.3 (abc1234)");
});
it("uses default tagline when cli.banner.taglineMode is default", () => {
loadConfigMock.mockReturnValue({
cli: { banner: { taglineMode: "default" } },
});
const line = formatCliBannerLine("2026.3.3", {
commit: "abc1234",
richTty: false,
});
expect(line).toBe("🦞 OpenClaw 2026.3.3 (abc1234) — All your chats, one OpenClaw.");
});
it("prefers explicit tagline mode over config", () => {
loadConfigMock.mockReturnValue({
cli: { banner: { taglineMode: "off" } },
});
const line = formatCliBannerLine("2026.3.3", {
commit: "abc1234",
richTty: false,
mode: "default",
});
expect(line).toBe("🦞 OpenClaw 2026.3.3 (abc1234) — All your chats, one OpenClaw.");
});
});

View File

@@ -1,8 +1,9 @@
import { loadConfig } from "../config/config.js";
import { resolveCommitHash } from "../infra/git-commit.js";
import { visibleWidth } from "../terminal/ansi.js";
import { isRich, theme } from "../terminal/theme.js";
import { hasRootVersionAlias } from "./argv.js";
import { pickTagline, type TaglineOptions } from "./tagline.js";
import { pickTagline, type TaglineMode, type TaglineOptions } from "./tagline.js";
type BannerOptions = TaglineOptions & {
argv?: string[];
@@ -35,18 +36,42 @@ const hasJsonFlag = (argv: string[]) =>
const hasVersionFlag = (argv: string[]) =>
argv.some((arg) => arg === "--version" || arg === "-V") || hasRootVersionAlias(argv);
function parseTaglineMode(value: unknown): TaglineMode | undefined {
if (value === "random" || value === "default" || value === "off") {
return value;
}
return undefined;
}
function resolveTaglineMode(options: BannerOptions): TaglineMode | undefined {
const explicit = parseTaglineMode(options.mode);
if (explicit) {
return explicit;
}
try {
return parseTaglineMode(loadConfig().cli?.banner?.taglineMode);
} catch {
// Fall back to default random behavior when config is missing/invalid.
return undefined;
}
}
export function formatCliBannerLine(version: string, options: BannerOptions = {}): string {
const commit = options.commit ?? resolveCommitHash({ env: options.env });
const commitLabel = commit ?? "unknown";
const tagline = pickTagline(options);
const tagline = pickTagline({ ...options, mode: resolveTaglineMode(options) });
const rich = options.richTty ?? isRich();
const title = "🦞 OpenClaw";
const prefix = "🦞 ";
const columns = options.columns ?? process.stdout.columns ?? 120;
const plainFullLine = `${title} ${version} (${commitLabel})${tagline}`;
const plainBaseLine = `${title} ${version} (${commitLabel})`;
const plainFullLine = tagline ? `${plainBaseLine}${tagline}` : plainBaseLine;
const fitsOnOneLine = visibleWidth(plainFullLine) <= columns;
if (rich) {
if (fitsOnOneLine) {
if (!tagline) {
return `${theme.heading(title)} ${theme.info(version)} ${theme.muted(`(${commitLabel})`)}`;
}
return `${theme.heading(title)} ${theme.info(version)} ${theme.muted(
`(${commitLabel})`,
)} ${theme.muted("—")} ${theme.accentDim(tagline)}`;
@@ -54,13 +79,19 @@ export function formatCliBannerLine(version: string, options: BannerOptions = {}
const line1 = `${theme.heading(title)} ${theme.info(version)} ${theme.muted(
`(${commitLabel})`,
)}`;
if (!tagline) {
return line1;
}
const line2 = `${" ".repeat(prefix.length)}${theme.accentDim(tagline)}`;
return `${line1}\n${line2}`;
}
if (fitsOnOneLine) {
return plainFullLine;
}
const line1 = `${title} ${version} (${commitLabel})`;
const line1 = plainBaseLine;
if (!tagline) {
return line1;
}
const line2 = `${" ".repeat(prefix.length)}${tagline}`;
return `${line1}\n${line2}`;
}

21
src/cli/tagline.test.ts Normal file
View File

@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_TAGLINE, pickTagline } from "./tagline.js";
describe("pickTagline", () => {
it("returns empty string when mode is off", () => {
expect(pickTagline({ mode: "off" })).toBe("");
});
it("returns default tagline when mode is default", () => {
expect(pickTagline({ mode: "default" })).toBe(DEFAULT_TAGLINE);
});
it("keeps OPENCLAW_TAGLINE_INDEX behavior in random mode", () => {
const value = pickTagline({
mode: "random",
env: { OPENCLAW_TAGLINE_INDEX: "0" } as NodeJS.ProcessEnv,
});
expect(value.length).toBeGreaterThan(0);
expect(value).not.toBe(DEFAULT_TAGLINE);
});
});

View File

@@ -1,4 +1,5 @@
const DEFAULT_TAGLINE = "All your chats, one OpenClaw.";
export type TaglineMode = "random" | "default" | "off";
const HOLIDAY_TAGLINES = {
newYear:
@@ -248,6 +249,7 @@ export interface TaglineOptions {
env?: NodeJS.ProcessEnv;
random?: () => number;
now?: () => Date;
mode?: TaglineMode;
}
export function activeTaglines(options: TaglineOptions = {}): string[] {
@@ -260,6 +262,12 @@ export function activeTaglines(options: TaglineOptions = {}): string[] {
}
export function pickTagline(options: TaglineOptions = {}): string {
if (options.mode === "off") {
return "";
}
if (options.mode === "default") {
return DEFAULT_TAGLINE;
}
const env = options.env ?? process.env;
const override = env?.OPENCLAW_TAGLINE_INDEX;
if (override !== undefined) {

View File

@@ -9,6 +9,7 @@ const ROOT_SECTIONS = [
"wizard",
"diagnostics",
"logging",
"cli",
"update",
"browser",
"ui",
@@ -421,6 +422,7 @@ const ENUM_EXPECTATIONS: Record<string, string[]> = {
],
"logging.consoleStyle": ['"pretty"', '"compact"', '"json"'],
"logging.redactSensitive": ['"off"', '"tools"'],
"cli.banner.taglineMode": ['"random"', '"default"', '"off"'],
"update.channel": ['"stable"', '"beta"', '"dev"'],
"agents.defaults.compaction.mode": ['"default"', '"safeguard"'],
"agents.defaults.compaction.identifierPolicy": ['"strict"', '"off"', '"custom"'],

View File

@@ -46,6 +46,11 @@ export const FIELD_HELP: Record<string, string> = {
'Sensitive redaction mode: "off" disables built-in masking, while "tools" redacts sensitive tool/config payload fields. Keep "tools" in shared logs unless you have isolated secure log sinks.',
"logging.redactPatterns":
"Additional custom redact regex patterns applied to log output before emission/storage. Use this to mask org-specific tokens and identifiers not covered by built-in redaction rules.",
cli: "CLI presentation controls for local command output behavior such as banner and tagline style. Use this section to keep startup output aligned with operator preference without changing runtime behavior.",
"cli.banner":
"CLI startup banner controls for title/version line and tagline style behavior. Keep banner enabled for fast version/context checks, then tune tagline mode to your preferred noise level.",
"cli.banner.taglineMode":
'Controls tagline style in the CLI startup banner: "random" (default) picks from the rotating tagline pool, "default" always shows the neutral default tagline, and "off" hides tagline text while keeping the banner version line.',
update:
"Update-channel and startup-check behavior for keeping OpenClaw runtime versions current. Use conservative channels in production and more experimental channels only in controlled environments.",
"update.channel": 'Update channel for git + npm installs ("stable", "beta", or "dev").',

View File

@@ -13,6 +13,7 @@ export type { ConfigUiHint, ConfigUiHints } from "../shared/config-ui-hints-type
const GROUP_LABELS: Record<string, string> = {
wizard: "Wizard",
update: "Update",
cli: "CLI",
diagnostics: "Diagnostics",
logging: "Logging",
gateway: "Gateway",
@@ -41,6 +42,7 @@ const GROUP_LABELS: Record<string, string> = {
const GROUP_ORDER: Record<string, number> = {
wizard: 20,
update: 25,
cli: 26,
diagnostics: 27,
gateway: 30,
nodeHost: 35,

View File

@@ -26,6 +26,9 @@ export const FIELD_LABELS: Record<string, string> = {
"logging.consoleStyle": "Console Log Style",
"logging.redactSensitive": "Sensitive Data Redaction Mode",
"logging.redactPatterns": "Custom Redaction Patterns",
cli: "CLI",
"cli.banner": "CLI Banner",
"cli.banner.taglineMode": "CLI Banner Tagline Mode",
update: "Updates",
"update.channel": "Update Channel",
"update.checkOnStart": "Update Check on Start",

13
src/config/types.cli.ts Normal file
View File

@@ -0,0 +1,13 @@
export type CliBannerTaglineMode = "random" | "default" | "off";
export type CliConfig = {
banner?: {
/**
* Controls CLI banner tagline behavior.
* - "random": pick from tagline pool (default)
* - "default": always use DEFAULT_TAGLINE
* - "off": hide tagline text
*/
taglineMode?: CliBannerTaglineMode;
};
};

View File

@@ -5,6 +5,7 @@ import type { AuthConfig } from "./types.auth.js";
import type { DiagnosticsConfig, LoggingConfig, SessionConfig, WebConfig } from "./types.base.js";
import type { BrowserConfig } from "./types.browser.js";
import type { ChannelsConfig } from "./types.channels.js";
import type { CliConfig } from "./types.cli.js";
import type { CronConfig } from "./types.cron.js";
import type {
CanvasHostConfig,
@@ -61,6 +62,7 @@ export type OpenClawConfig = {
};
diagnostics?: DiagnosticsConfig;
logging?: LoggingConfig;
cli?: CliConfig;
update?: {
/** Update channel for git + npm installs ("stable", "beta", or "dev"). */
channel?: "stable" | "beta" | "dev";

View File

@@ -8,6 +8,7 @@ export * from "./types.auth.js";
export * from "./types.base.js";
export * from "./types.browser.js";
export * from "./types.channels.js";
export * from "./types.cli.js";
export * from "./types.openclaw.js";
export * from "./types.cron.js";
export * from "./types.discord.js";

View File

@@ -222,6 +222,19 @@ export const OpenClawSchema = z
})
.strict()
.optional(),
cli: z
.object({
banner: z
.object({
taglineMode: z
.union([z.literal("random"), z.literal("default"), z.literal("off")])
.optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
update: z
.object({
channel: z.union([z.literal("stable"), z.literal("beta"), z.literal("dev")]).optional(),