feat: route clear sf chat commands
This commit is contained in:
parent
ab1a1edcf9
commit
47867c1236
5 changed files with 179 additions and 0 deletions
|
|
@ -109,4 +109,40 @@ describe("AgentSession custom message queueing", () => {
|
|||
assert.equal(followUps[0]?.role, "custom");
|
||||
assert.equal((followUps[0] as any).content, "after the current run");
|
||||
});
|
||||
|
||||
it("executes_slash_command_after_input_hook_transforms_chat_text", async () => {
|
||||
const session = await createSession();
|
||||
const agent = (session as any).agent as Agent & {
|
||||
prompt: (message: AgentMessage) => Promise<void>;
|
||||
};
|
||||
let agentPrompted = false;
|
||||
let commandArgs: string | undefined;
|
||||
agent.prompt = async () => {
|
||||
agentPrompted = true;
|
||||
};
|
||||
(session as any)._extensionRunner = {
|
||||
hasHandlers: (event: string) => event === "input",
|
||||
emitInput: async () => ({
|
||||
action: "transform",
|
||||
text: "/queue",
|
||||
images: undefined,
|
||||
}),
|
||||
getCommand: (name: string) =>
|
||||
name === "queue"
|
||||
? {
|
||||
name: "queue",
|
||||
handler: async (args: string) => {
|
||||
commandArgs = args;
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
createCommandContext: () => ({}),
|
||||
emitError: () => undefined,
|
||||
};
|
||||
|
||||
await session.prompt("show my queue");
|
||||
|
||||
assert.equal(commandArgs, "");
|
||||
assert.equal(agentPrompted, false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1223,6 +1223,16 @@ export class AgentSession {
|
|||
currentImages = inputResult.images ?? currentImages;
|
||||
}
|
||||
}
|
||||
if (
|
||||
expandPromptTemplates &&
|
||||
currentText !== text &&
|
||||
currentText.startsWith("/")
|
||||
) {
|
||||
const handled = await this._tryExecuteExtensionCommand(currentText);
|
||||
if (handled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Expand skill commands (/skill:name args) and prompt templates (/template args)
|
||||
let expandedText = currentText;
|
||||
|
|
|
|||
119
src/resources/extensions/sf/chat-command-router.js
Normal file
119
src/resources/extensions/sf/chat-command-router.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
const ROUTES = [
|
||||
{
|
||||
command: "help",
|
||||
patterns: [/\b(help|what can i do|commands?)\b/i],
|
||||
},
|
||||
{
|
||||
command: "queue",
|
||||
patterns: [
|
||||
/\b(show|open|view|list|see|what'?s in|what is in)\b.*\bqueue\b/i,
|
||||
/\bqueue\b.*\b(show|open|view|list)\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
command: "status",
|
||||
patterns: [
|
||||
/\b(show|open|view|check|what'?s|what is|where are we)\b.*\b(status|progress|dashboard)\b/i,
|
||||
/\bhow far\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
command: "history",
|
||||
patterns: [/\b(show|open|view|list)\b.*\b(history|what happened)\b/i],
|
||||
},
|
||||
{
|
||||
command: "logs",
|
||||
patterns: [/\b(show|open|view|tail|read)\b.*\b(logs?|debug logs?)\b/i],
|
||||
},
|
||||
{
|
||||
command: "forensics",
|
||||
patterns: [/\b(run|open|show|start)\b.*\bforensics?\b/i],
|
||||
},
|
||||
{
|
||||
command: "visualize",
|
||||
patterns: [/\b(open|show|view)\b.*\b(visuali[sz]e|visuali[sz]er|graph)\b/i],
|
||||
},
|
||||
{
|
||||
command: "doctor",
|
||||
patterns: [/\b(run|open|check)\b.*\b(doctor|health|diagnostics?)\b/i],
|
||||
},
|
||||
{
|
||||
command: "repair",
|
||||
patterns: [/\b(run|start|switch to)\b.*\brepair\b/i],
|
||||
},
|
||||
{
|
||||
command: "autonomous",
|
||||
patterns: [
|
||||
/\b(start|run|go|continue|resume)\b.*\b(autonomous|autonomy|auto mode)\b/i,
|
||||
/\bkeep going\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
command: "next",
|
||||
patterns: [/\b(run|do|take|start)\b.*\b(next step|next unit|one step)\b/i],
|
||||
},
|
||||
{
|
||||
command: "discuss",
|
||||
patterns: [
|
||||
/\b(start|open|enter)\b.*\b(discuss|discussion|planning|plan mode)\b/i,
|
||||
/\b(let'?s|lets)\b.*\b(discuss|plan)\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
command: "quick",
|
||||
patterns: [/\b(run|do|use)\b.*\bquick\b/i, /^quickly?\b/i],
|
||||
preserveArgs: true,
|
||||
},
|
||||
{
|
||||
command: "capture",
|
||||
patterns: [/\b(capture|remember|note)\b.*\b(this|that|idea|thought)\b/i],
|
||||
preserveArgs: true,
|
||||
},
|
||||
{
|
||||
command: "triage",
|
||||
patterns: [/\b(run|start|apply|do)\b.*\btriage\b/i],
|
||||
},
|
||||
{
|
||||
command: "pause",
|
||||
patterns: [/\b(pause|pause autonomous|take a break)\b/i],
|
||||
},
|
||||
{
|
||||
command: "stop",
|
||||
patterns: [/\b(stop|halt)\b.*\b(autonomous|run|sf)\b/i],
|
||||
},
|
||||
{
|
||||
command: "undo",
|
||||
patterns: [/\b(undo|revert|roll back)\b.*\b(last|previous|change|step)\b/i],
|
||||
},
|
||||
{
|
||||
command: "skip",
|
||||
patterns: [/\bskip\b.*\b(task|unit|this)\b/i],
|
||||
preserveArgs: true,
|
||||
},
|
||||
];
|
||||
|
||||
function normalizeArgs(text) {
|
||||
return text.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
export function routeChatCommand(text) {
|
||||
const input = normalizeArgs(text);
|
||||
if (!input || input.startsWith("/") || input.startsWith("!")) return null;
|
||||
if (input.length > 240) return null;
|
||||
|
||||
for (const route of ROUTES) {
|
||||
if (!route.patterns.some((pattern) => pattern.test(input))) continue;
|
||||
const args = route.preserveArgs ? input : "";
|
||||
return args ? `/${route.command} ${args}` : `/${route.command}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function registerChatCommandRouter(pi) {
|
||||
pi.on("input", (event) => {
|
||||
if (event.images?.length > 0) return { action: "continue" };
|
||||
const routed = routeChatCommand(event.text);
|
||||
if (!routed) return { action: "continue" };
|
||||
return { action: "transform", text: routed, images: event.images };
|
||||
});
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { getErrorMessage } from "./error-utils.js";
|
||||
|
||||
export {
|
||||
clearPendingGate,
|
||||
getPendingGate,
|
||||
|
|
@ -20,6 +21,10 @@ export default async function registerExtension(pi) {
|
|||
// tools, hooks) fails — e.g. due to a Windows-specific import error.
|
||||
const { registerSFCommands } = await import("./commands/index.js");
|
||||
registerSFCommands(pi);
|
||||
const { registerChatCommandRouter } = await import(
|
||||
"./chat-command-router.js"
|
||||
);
|
||||
registerChatCommandRouter(pi);
|
||||
|
||||
// Register steerable autonomous extension for Copilot Auto-style controls
|
||||
const { default: steerableAutonomousExtension } = await import(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
|||
import { join } from "node:path";
|
||||
import { test } from "vitest";
|
||||
import guardrails from "../../guardrails/index.js";
|
||||
import { routeChatCommand } from "../chat-command-router.js";
|
||||
import {
|
||||
DIRECT_SF_COMMAND_NAMES,
|
||||
getSfArgumentCompletions,
|
||||
|
|
@ -74,6 +75,14 @@ test("help_keyword_routes_natural_language_to_public_commands", () => {
|
|||
assert.doesNotMatch(messages[0], /\/parallel\b/);
|
||||
});
|
||||
|
||||
test("chat_command_router_maps_clear_chat_intent_to_public_workflow_commands", () => {
|
||||
assert.equal(routeChatCommand("I want to see my queue"), "/queue");
|
||||
assert.equal(routeChatCommand("start autonomous mode"), "/autonomous");
|
||||
assert.equal(routeChatCommand("let's discuss the architecture"), "/discuss");
|
||||
assert.equal(routeChatCommand("fix the login bug"), null);
|
||||
assert.equal(routeChatCommand("/queue"), null);
|
||||
});
|
||||
|
||||
test("direct command completions strip the already typed command name", () => {
|
||||
assert.deepEqual(getSfTopLevelCommandCompletions("autonomous", "--"), [
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue