fix: preserve gateway watch log colors

This commit is contained in:
Peter Steinberger
2026-05-02 07:00:02 +01:00
parent 0680c0b535
commit 0a798af4fc
4 changed files with 37 additions and 0 deletions

View File

@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/models: keep legacy CLI runtime model refs such as `claude-cli/*` in the configured allowlist after canonical runtime migration, so cron `payload.model` overrides keep working. Fixes #75753. Thanks @RyanSandoval.
- Gateway/watch: keep colored subsystem log prefixes in the managed tmux pane even when the parent shell exports `NO_COLOR`, while preserving explicit `FORCE_COLOR=0` opt-out. Thanks @vincentkoc.
- Plugin SDK: re-export `isPrivateIpAddress` from `plugin-sdk/ssrf-runtime`, restoring source-checkout builds for SearXNG and Firecrawl private-network guards. Thanks @vincentkoc.
- CLI/directory: report unsupported directory operations for installed channel plugins instead of prompting to reinstall the plugin when it lacks a directory adapter. Fixes #75770. Thanks @lawong888.
- Web search/SearXNG: show the JSON API `search.formats` prerequisite during SearXNG setup before prompting for the base URL. Supersedes #65592. Thanks @evanpaul14.

View File

@@ -251,6 +251,8 @@ The tmux wrapper carries common non-secret runtime selectors such as
`OPENCLAW_GATEWAY_PORT`, and `OPENCLAW_SKIP_CHANNELS` into the pane. Put
provider credentials in your normal profile/config, or use raw foreground mode
for one-off ephemeral secrets.
The managed tmux pane also defaults to colored Gateway logs for readability;
set `FORCE_COLOR=0` when starting `pnpm gateway:watch` to disable ANSI output.
The watcher restarts on build-relevant files under `src/`, extension source files,
extension `package.json` and `openclaw.plugin.json` metadata, `tsconfig.json`,

View File

@@ -66,6 +66,17 @@ export const resolveGatewayWatchTmuxSessionName = ({ args = [], env = process.en
const resolveShell = (env) => env.SHELL || "/bin/sh";
const resolveColorEnv = (env) => {
const forceColor = env.FORCE_COLOR;
if (forceColor == null || forceColor === "") {
return { assignments: ["FORCE_COLOR=1"], options: ["-u", "NO_COLOR"] };
}
if (String(forceColor).trim() !== "0") {
return { assignments: [`FORCE_COLOR=${forceColor}`], options: ["-u", "NO_COLOR"] };
}
return { assignments: [`FORCE_COLOR=${forceColor}`], options: [] };
};
export const buildGatewayWatchTmuxCommand = ({
args = [],
cwd = process.cwd(),
@@ -74,10 +85,13 @@ export const buildGatewayWatchTmuxCommand = ({
sessionName,
} = {}) => {
const shell = resolveShell(env);
const colorEnv = resolveColorEnv(env);
const childEnv = [
"env",
...colorEnv.options,
`OPENCLAW_GATEWAY_WATCH_TMUX_CHILD=1`,
`OPENCLAW_GATEWAY_WATCH_SESSION=${sessionName}`,
...colorEnv.assignments,
...TMUX_CHILD_ENV_KEYS.flatMap((key) =>
env[key] == null || env[key] === "" ? [] : [`${key}=${env[key]}`],
),

View File

@@ -53,6 +53,8 @@ describe("gateway-watch tmux wrapper", () => {
expect(command).toContain("/repo with spaces/openclaw");
expect(command).toContain("'OPENCLAW_GATEWAY_WATCH_TMUX_CHILD=1'");
expect(command).toContain("'OPENCLAW_GATEWAY_WATCH_SESSION=openclaw-gateway-watch-main'");
expect(command).toContain("'\\''-u'\\'' '\\''NO_COLOR'\\''");
expect(command).toContain("'FORCE_COLOR=1'");
expect(command).toContain("'OPENCLAW_GATEWAY_PORT=19001'");
expect(command).toContain("'OPENCLAW_PROFILE=Dev Profile'");
expect(command).toContain("/opt/node");
@@ -62,6 +64,24 @@ describe("gateway-watch tmux wrapper", () => {
expect(command).toContain("'a b.jsonl'");
});
it("preserves an explicit color override for the tmux child", () => {
const command = buildGatewayWatchTmuxCommand({
args: ["gateway", "--force"],
cwd: "/repo",
env: {
FORCE_COLOR: "0",
NO_COLOR: "1",
SHELL: "/bin/zsh",
},
nodePath: "/opt/node",
sessionName: "openclaw-gateway-watch-main",
});
expect(command).toContain("'FORCE_COLOR=0'");
expect(command).not.toContain("'\\''-u'\\'' '\\''NO_COLOR'\\''");
expect(command).not.toContain("'FORCE_COLOR=1'");
});
it("creates a detached tmux session when none exists", () => {
const stdout = createOutput();
const stderr = createOutput();