mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: disambiguate inline skills from absolute paths
This commit is contained in:
@@ -529,7 +529,7 @@ See [BTW side questions](/tools/btw) for the full behavior.
|
||||
- Command-only messages from allowlisted senders are handled immediately (bypass queue + model).
|
||||
- Inline shortcuts (`/help`, `/commands`, `/status`, `/whoami`) also work embedded in normal messages and are stripped before the model sees the remaining text.
|
||||
- In Control UI, selecting an inline shortcut runs it separately and removes only that token from the composer; the surrounding draft remains unsent.
|
||||
- Skill commands can be embedded in normal messages (for example, `Please use /weather to check Sydney`). The skill marker selects the skill and the surrounding text becomes its input.
|
||||
- Skill commands can be embedded in normal messages with a colon-terminated marker (for example, `Please use /weather: to check Sydney`). The skill marker selects the skill and the surrounding text becomes its input; bare root-style tokens such as `/weather` remain ordinary text unless selected in the Control UI.
|
||||
- Session-changing control commands such as `/reset` remain command-only. This prevents prose that merely mentions a command from executing it.
|
||||
- Unauthorized command-only messages are silently ignored; inline `/...` tokens are treated as plain text.
|
||||
|
||||
|
||||
@@ -761,7 +761,7 @@ describe("handleInlineActions", () => {
|
||||
|
||||
it("rewrites a skill marker embedded in normal prose", async () => {
|
||||
const typing = createTypingController();
|
||||
const body = "Please use /office_hours to build me a deployment plan";
|
||||
const body = "Please use /office_hours: to build me a deployment plan";
|
||||
const ctx = buildTestCtx({ Body: body, CommandBody: body });
|
||||
|
||||
const result = await handleInlineActions(
|
||||
@@ -795,7 +795,7 @@ describe("handleInlineActions", () => {
|
||||
|
||||
it("keeps unauthorized inline skill markers as plain text", async () => {
|
||||
const typing = createTypingController();
|
||||
const body = "Please use /office_hours to build me a deployment plan";
|
||||
const body = "Please use /office_hours: to build me a deployment plan";
|
||||
const ctx = buildTestCtx({ Body: body, CommandBody: body });
|
||||
|
||||
const result = await handleInlineActions(
|
||||
|
||||
@@ -129,7 +129,7 @@ function resolveInlineSkillCommandInvocation(params: {
|
||||
skillCommands: SkillCommandSpec[];
|
||||
}): { command: SkillCommandSpec; args?: string; inline: true } | null {
|
||||
const body = params.commandBodyNormalized;
|
||||
const directPattern = /(?:^|\s)\/([^\s:]+)(?=$|\s|:)(?:\s*:\s*)?/giu;
|
||||
const directPattern = /(?:^|\s)\/([^\s:]+)\s*:\s*/giu;
|
||||
for (const match of body.matchAll(directPattern)) {
|
||||
const rawName = match[1] ?? "";
|
||||
if (normalizeOptionalLowercaseString(rawName) === "skill") {
|
||||
@@ -146,7 +146,7 @@ function resolveInlineSkillCommandInvocation(params: {
|
||||
return { command, args: args || undefined, inline: true };
|
||||
}
|
||||
|
||||
const skillPattern = /(?:^|\s)\/skill(?=$|\s|:)(?:\s*:\s*|\s+)([^\s:]+)/giu;
|
||||
const skillPattern = /(?:^|\s)\/skill\s*:\s*([^\s:]+)/giu;
|
||||
for (const match of body.matchAll(skillPattern)) {
|
||||
const command = findSkillCommand(params.skillCommands, match[1] ?? "");
|
||||
if (!command || command.modelVisible === false || match.index === undefined) {
|
||||
|
||||
@@ -212,7 +212,7 @@ describe("resolveSkillCommandInvocation", () => {
|
||||
|
||||
it("matches direct skill invocations embedded in a sentence", () => {
|
||||
const invocation = resolveSkillCommandInvocation({
|
||||
commandBodyNormalized: "Please use /demo_skill to do the thing",
|
||||
commandBodyNormalized: "Please use /demo_skill: to do the thing",
|
||||
skillCommands: [{ name: "demo_skill", skillName: "demo-skill", description: "Demo" }],
|
||||
});
|
||||
expect(invocation?.command.skillName).toBe("demo-skill");
|
||||
@@ -222,7 +222,7 @@ describe("resolveSkillCommandInvocation", () => {
|
||||
|
||||
it("matches /skill invocations embedded in a sentence", () => {
|
||||
const invocation = resolveSkillCommandInvocation({
|
||||
commandBodyNormalized: "Please ask /skill demo_skill about this",
|
||||
commandBodyNormalized: "Please ask /skill:demo_skill about this",
|
||||
skillCommands: [{ name: "demo_skill", skillName: "demo-skill", description: "Demo" }],
|
||||
});
|
||||
expect(invocation?.command.name).toBe("demo_skill");
|
||||
@@ -230,7 +230,7 @@ describe("resolveSkillCommandInvocation", () => {
|
||||
expect(invocation?.inline).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["Please use /hidden_skill for this", "Please use /skill hidden_skill for this"])(
|
||||
it.each(["Please use /hidden_skill: for this", "Please use /skill:hidden_skill for this"])(
|
||||
"does not resolve model-hidden inline skill invocations in %j",
|
||||
(commandBodyNormalized) => {
|
||||
expect(
|
||||
@@ -263,6 +263,12 @@ describe("resolveSkillCommandInvocation", () => {
|
||||
skillCommands,
|
||||
}),
|
||||
).toBeNull();
|
||||
expect(
|
||||
resolveSkillCommandInvocation({
|
||||
commandBodyNormalized: "Open /demo_skill please",
|
||||
skillCommands,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("normalizes /skill lookup names", () => {
|
||||
|
||||
@@ -3749,7 +3749,7 @@ describe("chat slash menu accessibility", () => {
|
||||
keydownComposer(container, "Enter");
|
||||
|
||||
expect(onSlashCommand).not.toHaveBeenCalled();
|
||||
expect(draft).toBe("Please use /weather ");
|
||||
expect(draft).toBe("Please use /weather: ");
|
||||
});
|
||||
|
||||
it("does not offer session-changing commands in the middle of prose", () => {
|
||||
|
||||
@@ -298,6 +298,7 @@ export function selectSlashCommand(
|
||||
requestUpdate: () => void,
|
||||
) {
|
||||
const state = getChatComposerState(props.paneId);
|
||||
const inlineReplacement = cmd.source === "skill" ? `/${cmd.name}:` : `/${cmd.name}`;
|
||||
if (beginInlineSlashArguments(cmd, props, state, requestUpdate)) {
|
||||
return;
|
||||
}
|
||||
@@ -314,7 +315,7 @@ export function selectSlashCommand(
|
||||
props.onSlashCommand(`/${cmd.name}`);
|
||||
return;
|
||||
}
|
||||
if (commitInlineSlashSelection(`/${cmd.name}`, props, state)) {
|
||||
if (commitInlineSlashSelection(inlineReplacement, props, state)) {
|
||||
state.slashMenuOpen = false;
|
||||
resetSlashMenuState(state);
|
||||
requestUpdate();
|
||||
@@ -349,10 +350,11 @@ export function tabCompleteSlashCommand(
|
||||
requestUpdate: () => void,
|
||||
) {
|
||||
const state = getChatComposerState(props.paneId);
|
||||
const inlineReplacement = cmd.source === "skill" ? `/${cmd.name}:` : `/${cmd.name}`;
|
||||
if (beginInlineSlashArguments(cmd, props, state, requestUpdate)) {
|
||||
return;
|
||||
}
|
||||
if (commitInlineSlashSelection(`/${cmd.name}`, props, state)) {
|
||||
if (commitInlineSlashSelection(inlineReplacement, props, state)) {
|
||||
state.slashMenuOpen = false;
|
||||
resetSlashMenuState(state);
|
||||
requestUpdate();
|
||||
|
||||
Reference in New Issue
Block a user