fix: gate tui host footer behind config

This commit is contained in:
Shakker 2026-06-09 17:17:17 +01:00 committed by Shakker
parent 479e2aaae3
commit d48778994f
13 changed files with 131 additions and 14 deletions

View file

@ -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:<id>:...`).
- 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.

View file

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

View file

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

View file

@ -15,6 +15,7 @@ const ROOT_SECTIONS = [
"commitments",
"browser",
"ui",
"tui",
"auth",
"models",
"nodeHost",

View file

@ -1354,6 +1354,11 @@ export const FIELD_HELP: Record<string, string> = {
"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":

View file

@ -786,6 +786,9 @@ export const FIELD_LABELS: Record<string, string> = {
"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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<TuiResult> {
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}`,