Skip to content

Commit c9e1187

Browse files
author
HCL
committed
fix(commands): preserve multiline args after slash command head (#79155)
normalizeCommandBody() discarded everything after the first newline when normalizing a command like: /some-command line 1 line 2 Root cause: singleLine = trimmed.slice(0, newline) dropped the tail before alias expansion and token dispatch. Fix: capture multilineTail = trimmed.slice(newline) before normalization, then re-attach it to the final resolved head at all return paths: - exact alias match - unregistered token fallback - no-args token (acceptsArgs=false) fallback - resolved canonical + args return The command head (first line) is still normalized for alias expansion and bot-mention stripping. The tail passes through verbatim so callers receive the full multiline payload. 27/27 commands-registry tests pass, including new regression test. Fixes #79155
1 parent 5ae385b commit c9e1187

3 files changed

Lines changed: 22 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
88

99
- Agents/failover: harden state-aware lane suspension by persisting quota resume transitions, restoring configured lane concurrency, preserving non-quota failure reasons, and exporting model failover events through diagnostics OTLP. Thanks @BunsDev.
1010
- Channels/streaming: make progress draft labels scroll away with other progress lines, render structured tool rows as compact emoji/title/details, show web-search queries from provider-native argument shapes, and skip empty Discord apply-patch starts until a patch summary exists. (#79146)
11+
- Commands/skills: preserve multiline arguments after a slash command head so payloads like `/some-command\nline 1\nline 2` deliver all lines to the handler instead of only the first. (#79155)
1112
- Telegram: preserve the channel-specific 10-option poll cap in the unified outbound adapter so over-limit polls are rejected before send. (#78762) Thanks @obviyus.
1213
- Runtime/install: raise the supported Node 22 floor to `22.16+` so native SQLite query handling can rely on the `node:sqlite` statement metadata API while continuing to recommend Node 24. (#78921)
1314
- Discord/voice: include a bounded one-line STT transcript preview in verbose voice logs so live voice debugging shows what speakers said before the agent reply.

src/auto-reply/commands-registry-normalize.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export function normalizeCommandBody(raw: string, options?: CommandNormalizeOpti
5757

5858
const newline = trimmed.indexOf("\n");
5959
const singleLine = newline === -1 ? trimmed : trimmed.slice(0, newline).trim();
60+
const multilineTail = newline === -1 ? "" : trimmed.slice(newline);
6061

6162
const colonMatch = singleLine.match(/^\/([^\s:]+)\s*:(.*)$/);
6263
const normalized = colonMatch
@@ -80,24 +81,27 @@ export function normalizeCommandBody(raw: string, options?: CommandNormalizeOpti
8081
const textAliasMap = getTextAliasMap();
8182
const exact = textAliasMap.get(lowered);
8283
if (exact) {
83-
return exact.canonical;
84+
return exact.canonical + multilineTail;
8485
}
8586

8687
const tokenMatch = commandBody.match(/^\/([^\s]+)(?:\s+([\s\S]+))?$/);
8788
if (!tokenMatch) {
88-
return commandBody;
89+
return commandBody + multilineTail;
8990
}
9091
const [, token, rest] = tokenMatch;
9192
const tokenKey = `/${normalizeLowercaseStringOrEmpty(token)}`;
9293
const tokenSpec = textAliasMap.get(tokenKey);
9394
if (!tokenSpec) {
94-
return commandBody;
95+
return commandBody + multilineTail;
9596
}
9697
if (rest && !tokenSpec.acceptsArgs) {
97-
return commandBody;
98+
return commandBody + multilineTail;
9899
}
99100
const normalizedRest = rest?.trimStart();
100-
return normalizedRest ? `${tokenSpec.canonical} ${normalizedRest}` : tokenSpec.canonical;
101+
const resolvedHead = normalizedRest
102+
? `${tokenSpec.canonical} ${normalizedRest}`
103+
: tokenSpec.canonical;
104+
return resolvedHead + multilineTail;
101105
}
102106

103107
export function getCommandDetection(_cfg?: OpenClawConfig): CommandDetection {

src/auto-reply/commands-registry.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,18 @@ describe("commands registry", () => {
405405
it("keeps unregistered dock underscore aliases unchanged", () => {
406406
expect(normalizeCommandBody("/dock_telegram")).toBe("/dock_telegram");
407407
});
408+
409+
it("preserves multiline args after command head — regression for #79155", () => {
410+
expect(normalizeCommandBody("/btw\nfirst line\nsecond line")).toBe(
411+
"/btw\nfirst line\nsecond line",
412+
);
413+
expect(normalizeCommandBody("/help\nline 1\nline 2\nline 3")).toBe(
414+
"/help\nline 1\nline 2\nline 3",
415+
);
416+
expect(
417+
normalizeCommandBody("/help@openclaw\nmultiline args", { botUsername: "openclaw" }),
418+
).toBe("/help\nmultiline args");
419+
});
408420
});
409421

410422
describe("commands registry args", () => {

0 commit comments

Comments
 (0)