diff --git a/docs/cli/tui.md b/docs/cli/tui.md index a4cdd59860f..a19fe435dae 100644 --- a/docs/cli/tui.md +++ b/docs/cli/tui.md @@ -40,6 +40,7 @@ Notes: - `--local` cannot be combined with `--url`, `--token`, or `--password`. - `tui` resolves configured gateway auth SecretRefs for token/password auth when possible (`env`/`file`/`exec` providers). - When launched from inside a configured agent workspace directory, TUI auto-selects that agent for the session key default (unless `--session` is explicitly `agent::...`). +- To show the Gateway hostname in the footer for non-local URL-backed connections, run `openclaw config set tui.footer.showRemoteHost true`. The host label is off by default and never appears for loopback or embedded local connections. - Local mode uses the embedded agent runtime directly. Most local tools work, but Gateway-only features are unavailable. - Local mode adds `/auth [provider]` inside the TUI command surface. - Plugin approval gates still apply in local mode. Tools that require approval prompt for a decision in the terminal; nothing is silently auto-approved because the Gateway is not involved. diff --git a/docs/web/tui.md b/docs/web/tui.md index 252d133c7ce..dce135ef7ef 100644 --- a/docs/web/tui.md +++ b/docs/web/tui.md @@ -54,7 +54,7 @@ Notes: - Header: connection URL, current agent, current session. - Chat log: user messages, assistant replies, system notices, tool cards. - Status line: connection/run state (connecting, running, streaming, idle, error). -- Footer: connection host when available + agent + session + model + goal state + think/fast/verbose/trace/reasoning + token counts + deliver. +- Footer: agent + session + model + goal state + think/fast/verbose/trace/reasoning + token counts + deliver. When `tui.footer.showRemoteHost` is enabled, remote Gateway connections also show the connection host. - Input: text editor with autocomplete. ## Mental model: agents + sessions @@ -67,7 +67,15 @@ Notes: - Session scope: - `per-sender` (default): each agent has many sessions. - `global`: the TUI always uses the `global` session (the picker may be empty). -- For URL-backed connections, the footer includes the connection host alongside the current agent and session. +- The current agent + session are always visible in the footer. +- To show the Gateway host for non-local URL-backed connections, opt in with: + + ```bash + openclaw config set tui.footer.showRemoteHost true + ``` + + Loopback and embedded local connections never show a host label. + - If the session has a [goal](/tools/goal), the footer shows its compact state such as `Pursuing goal`, `Goal paused (/goal resume)`, or `Goal achieved`. diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index fb6404a81f8..7d326c738be 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -479,6 +479,32 @@ describe("ui.seamColor", () => { }); }); +describe("tui.footer.showRemoteHost", () => { + it("accepts the TUI remote-host footer toggle", () => { + const result = OpenClawSchema.safeParse({ + tui: { + footer: { + showRemoteHost: true, + }, + }, + }); + + expect(result.success).toBe(true); + }); + + it("rejects unknown TUI footer keys", () => { + const result = OpenClawSchema.safeParse({ + tui: { + footer: { + showLocalHost: true, + }, + }, + }); + + expect(result.success).toBe(false); + }); +}); + describe("gateway.controlUi.embedSandbox", () => { it("accepts strict, scripts, and trusted modes", () => { for (const mode of ["strict", "scripts", "trusted"] as const) { diff --git a/src/config/schema.help.quality.test.ts b/src/config/schema.help.quality.test.ts index 7bf2fdd405a..fc3cdbb2fb1 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -15,6 +15,7 @@ const ROOT_SECTIONS = [ "commitments", "browser", "ui", + "tui", "auth", "models", "nodeHost", diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index bfd6b99409c..9f2a0de4732 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -1354,6 +1354,11 @@ export const FIELD_HELP: Record = { "Display name shown for the assistant in UI views, chat chrome, and status contexts. Keep this stable so operators can reliably identify which assistant persona is active.", "ui.assistant.avatar": "Assistant avatar image source used in UI surfaces (URL, path, or data URI depending on runtime support). Use trusted assets and consistent branding dimensions for clean rendering.", + tui: "Terminal UI display settings. Use this section for terminal-only presentation preferences without changing Gateway or other UI behavior.", + "tui.footer": + "Terminal UI footer display settings. Keep optional context compact so session, model, goal, and token information stay readable.", + "tui.footer.showRemoteHost": + "Show the remote Gateway hostname in the TUI footer for non-local URL-backed connections. Default: false. Loopback and embedded local connections never show a host label.", plugins: "Plugin system controls for enabling extensions, constraining load scope, configuring entries, and tracking installs. Keep plugin policy explicit and least-privilege in production environments.", "plugins.enabled": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 462a31df30f..ccff90c6061 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -786,6 +786,9 @@ export const FIELD_LABELS: Record = { "ui.assistant": "Assistant Appearance", "ui.assistant.name": "Assistant Name", "ui.assistant.avatar": "Assistant Avatar", + tui: "Terminal UI", + "tui.footer": "Terminal UI Footer", + "tui.footer.showRemoteHost": "Show Remote Host in TUI Footer", "browser.evaluateEnabled": "Browser Evaluate Enabled", "browser.snapshotDefaults": "Browser Snapshot Defaults", "browser.snapshotDefaults.mode": "Browser Snapshot Mode", diff --git a/src/config/types.openclaw.ts b/src/config/types.openclaw.ts index a12c4df5b5e..faf20921d24 100644 --- a/src/config/types.openclaw.ts +++ b/src/config/types.openclaw.ts @@ -166,6 +166,14 @@ export type OpenClawConfig = { avatar?: string; }; }; + /** Terminal UI display settings. */ + tui?: { + /** Footer display settings for the terminal UI. */ + footer?: { + /** Show the remote Gateway hostname in the footer for non-local URL-backed connections. */ + showRemoteHost?: boolean; + }; + }; /** Secret providers, defaults, and ref-resolution settings. */ secrets?: SecretsConfig; /** Skill loading and bundled skill configuration. */ diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index fc7109b12b3..28d97abf85e 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -709,6 +709,17 @@ export const OpenClawSchema = z }) .strict() .optional(), + tui: z + .object({ + footer: z + .object({ + showRemoteHost: z.boolean().optional(), + }) + .strict() + .optional(), + }) + .strict() + .optional(), secrets: SecretsConfigSchema, auth: z .object({ diff --git a/src/tui/tui-formatters.test.ts b/src/tui/tui-formatters.test.ts index 995130f1702..31eb960644a 100644 --- a/src/tui/tui-formatters.test.ts +++ b/src/tui/tui-formatters.test.ts @@ -5,8 +5,8 @@ import { extractContentFromMessage, extractTextFromMessage, extractThinkingFromMessage, - formatConnectionHostFooter, formatGoalFooter, + formatRemoteConnectionHostFooter, isCommandMessage, sanitizeRenderableText, } from "./tui-formatters.js"; @@ -46,16 +46,20 @@ describe("formatGoalFooter", () => { }); }); -describe("formatConnectionHostFooter", () => { - it("renders only the connection hostname", () => { - expect(formatConnectionHostFooter("ws://gateway-host:18789")).toBe("host gateway-host"); +describe("formatRemoteConnectionHostFooter", () => { + it("renders only the remote connection hostname", () => { + expect(formatRemoteConnectionHostFooter("ws://gateway-host:18789")).toBe("host gateway-host"); expect( - formatConnectionHostFooter("wss://user:secret@example.com:443/path?token=redacted"), + formatRemoteConnectionHostFooter("wss://user:secret@example.com:443/path?token=redacted"), ).toBe("host example.com"); }); - it("skips non-url local connection labels", () => { - expect(formatConnectionHostFooter("local embedded")).toBeNull(); + it("skips local and non-url connection labels", () => { + expect(formatRemoteConnectionHostFooter("local embedded")).toBeNull(); + expect(formatRemoteConnectionHostFooter("ws://localhost:18789")).toBeNull(); + expect(formatRemoteConnectionHostFooter("ws://127.0.0.1:18789")).toBeNull(); + expect(formatRemoteConnectionHostFooter("ws://127.1:18789")).toBeNull(); + expect(formatRemoteConnectionHostFooter("ws://[::1]:18789")).toBeNull(); }); }); diff --git a/src/tui/tui-formatters.ts b/src/tui/tui-formatters.ts index e6238b7d90f..ee0e64d020d 100644 --- a/src/tui/tui-formatters.ts +++ b/src/tui/tui-formatters.ts @@ -2,6 +2,7 @@ import { stripAnsi } from "../../packages/terminal-core/src/ansi.js"; import { stripLeadingInboundMetadata } from "../auto-reply/reply/strip-inbound-meta.js"; import type { SessionGoal } from "../config/sessions/types.js"; +import { isLoopbackHost } from "../gateway/net.js"; import { formatRawAssistantErrorForUi } from "../shared/assistant-error-format.js"; import { extractAssistantVisibleText } from "../shared/chat-message-content.js"; import { formatTokenCount } from "../utils/usage-format.js"; @@ -443,10 +444,10 @@ export function formatTokens(total?: number | null, context?: number | null) { return `tokens ${totalLabel}/${formatTokenCount(context)}${pct !== null ? ` (${pct}%)` : ""}`; } -export function formatConnectionHostFooter(connectionUrl: string): string | null { +export function formatRemoteConnectionHostFooter(connectionUrl: string): string | null { try { const hostname = new URL(connectionUrl.trim()).hostname.trim(); - return hostname ? `host ${hostname}` : null; + return hostname && !isLoopbackHost(hostname) ? `host ${hostname}` : null; } catch { return null; } diff --git a/src/tui/tui-pty-harness.e2e.test.ts b/src/tui/tui-pty-harness.e2e.test.ts index 121c3ec8eb9..9be5f2da9ee 100644 --- a/src/tui/tui-pty-harness.e2e.test.ts +++ b/src/tui/tui-pty-harness.e2e.test.ts @@ -364,7 +364,7 @@ describe.sequential("TUI PTY harness", () => { it("renders local ready on startup", () => { expect(fixture.run.output()).toContain("local ready"); - expect(fixture.run.output()).toContain("host local"); + expect(fixture.run.output()).not.toContain("host local"); }); it( diff --git a/src/tui/tui.test.ts b/src/tui/tui.test.ts index af398779725..1f07834c7f5 100644 --- a/src/tui/tui.test.ts +++ b/src/tui/tui.test.ts @@ -23,6 +23,7 @@ import { resolveLocalAuthSpawnCwd, resolveLocalAuthSpawnOptions, resolveTuiCtrlCAction, + resolveTuiFooterHostLabel, resolveTuiShutdownHardExitMs, resolveTuiSessionKey, scheduleProcessExitAfterTuiReturn, @@ -64,6 +65,40 @@ describe("resolveFinalAssistantText", () => { }); }); +describe("resolveTuiFooterHostLabel", () => { + it("hides connection host by default", () => { + expect( + resolveTuiFooterHostLabel({ + config: {}, + connectionUrl: "wss://gateway.example.com/ws", + }), + ).toBeNull(); + }); + + it("renders only remote hosts when explicitly enabled", () => { + const config = { tui: { footer: { showRemoteHost: true } } } satisfies OpenClawConfig; + + expect( + resolveTuiFooterHostLabel({ + config, + connectionUrl: "wss://user:secret@gateway.example.com/ws?token=hidden", + }), + ).toBe("host gateway.example.com"); + expect( + resolveTuiFooterHostLabel({ + config, + connectionUrl: "ws://127.0.0.1:18789", + }), + ).toBeNull(); + expect( + resolveTuiFooterHostLabel({ + config, + connectionUrl: "local embedded", + }), + ).toBeNull(); + }); +}); + describe("tui slash commands", () => { it("treats /elev as an alias for /elevated", () => { expect(parseCommand("/elev on")).toEqual({ name: "elevated", args: "on" }); diff --git a/src/tui/tui.ts b/src/tui/tui.ts index 1a488ca9d6e..31cce229805 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -35,7 +35,11 @@ import { editorTheme, theme } from "./theme/theme.js"; import type { TuiBackend } from "./tui-backend.js"; import { createCommandHandlers } from "./tui-command-handlers.js"; import { createEventHandlers } from "./tui-event-handlers.js"; -import { formatConnectionHostFooter, formatGoalFooter, formatTokens } from "./tui-formatters.js"; +import { + formatGoalFooter, + formatRemoteConnectionHostFooter, + formatTokens, +} from "./tui-formatters.js"; import { buildTuiLastSessionScopeKey, readTuiLastSessionKey, @@ -171,6 +175,16 @@ export function resolveTuiSessionKey(params: { return `agent:${params.currentAgentId}:${normalizeLowercaseStringOrEmpty(trimmed)}`; } +export function resolveTuiFooterHostLabel(params: { + config: OpenClawConfig; + connectionUrl: string; +}): string | null { + if (params.config.tui?.footer?.showRemoteHost !== true) { + return null; + } + return formatRemoteConnectionHostFooter(params.connectionUrl); +} + export function resolveInitialTuiAgentId(params: { cfg: OpenClawConfig; fallbackAgentId: string; @@ -1199,7 +1213,7 @@ export async function runTui(opts: RunTuiOptions): Promise { const reasoning = sessionInfo.reasoningLevel ?? "off"; const reasoningLabel = reasoning === "on" ? "reasoning" : reasoning === "stream" ? "reasoning:stream" : null; - const hostLabel = formatConnectionHostFooter(client.connection.url); + const hostLabel = resolveTuiFooterHostLabel({ config, connectionUrl: client.connection.url }); const footerParts = [ hostLabel, `agent ${agentLabel}`,