mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:01:38 +00:00
feat: improve /help and /commands formatting with categories and pagination
- Add CommandCategory type to organize commands into groups (session, options, status, management, media, tools, docks) - Refactor /help to show grouped sections for better discoverability - Add pagination support for /commands on Telegram (8 commands per page with nav buttons) - Show grouped list without pagination on other channels - Handle commands_page_N callback queries for Telegram pagination navigation
This commit is contained in:
committed by
Gustavo Madeira Santana
parent
d3a6333ef7
commit
d91b4a3045
@@ -29,7 +29,12 @@ import {
|
||||
resolveModelCostConfig,
|
||||
} from "../utils/usage-format.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { listChatCommands, listChatCommandsForConfig } from "./commands-registry.js";
|
||||
import {
|
||||
listChatCommands,
|
||||
listChatCommandsForConfig,
|
||||
type ChatCommandDefinition,
|
||||
} from "./commands-registry.js";
|
||||
import type { CommandCategory } from "./commands-registry.types.js";
|
||||
import { listPluginCommands } from "../plugins/commands.js";
|
||||
import type { SkillCommandSpec } from "../agents/skills.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js";
|
||||
@@ -427,61 +432,248 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
const CATEGORY_LABELS: Record<CommandCategory, string> = {
|
||||
session: "Session",
|
||||
options: "Options",
|
||||
status: "Status",
|
||||
management: "Management",
|
||||
media: "Media",
|
||||
tools: "Tools",
|
||||
docks: "Docks",
|
||||
};
|
||||
|
||||
const CATEGORY_ORDER: CommandCategory[] = [
|
||||
"session",
|
||||
"options",
|
||||
"status",
|
||||
"management",
|
||||
"media",
|
||||
"tools",
|
||||
"docks",
|
||||
];
|
||||
|
||||
function groupCommandsByCategory(
|
||||
commands: ChatCommandDefinition[],
|
||||
): Map<CommandCategory, ChatCommandDefinition[]> {
|
||||
const grouped = new Map<CommandCategory, ChatCommandDefinition[]>();
|
||||
for (const category of CATEGORY_ORDER) {
|
||||
grouped.set(category, []);
|
||||
}
|
||||
for (const command of commands) {
|
||||
const category = command.category ?? "tools";
|
||||
const list = grouped.get(category) ?? [];
|
||||
list.push(command);
|
||||
grouped.set(category, list);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
export function buildHelpMessage(cfg?: ClawdbotConfig): string {
|
||||
const options = [
|
||||
"/think <level>",
|
||||
"/verbose on|full|off",
|
||||
"/reasoning on|off",
|
||||
"/elevated on|off|ask|full",
|
||||
"/model <id>",
|
||||
"/usage off|tokens|full",
|
||||
];
|
||||
if (cfg?.commands?.config === true) options.push("/config show");
|
||||
if (cfg?.commands?.debug === true) options.push("/debug show");
|
||||
return [
|
||||
"ℹ️ Help",
|
||||
"Shortcuts: /new reset | /compact [instructions] | /restart relink (if enabled)",
|
||||
`Options: ${options.join(" | ")}`,
|
||||
"Skills: /skill <name> [input]",
|
||||
"More: /commands for all slash commands",
|
||||
].join("\n");
|
||||
const lines = ["ℹ️ Help", ""];
|
||||
|
||||
// Session commands - quick shortcuts
|
||||
lines.push("Session");
|
||||
lines.push(" /new | /reset | /compact [instructions] | /stop");
|
||||
lines.push("");
|
||||
|
||||
// Options - most commonly used
|
||||
const optionParts = ["/think <level>", "/model <id>", "/verbose on|off"];
|
||||
if (cfg?.commands?.config === true) optionParts.push("/config");
|
||||
if (cfg?.commands?.debug === true) optionParts.push("/debug");
|
||||
lines.push("Options");
|
||||
lines.push(` ${optionParts.join(" | ")}`);
|
||||
lines.push("");
|
||||
|
||||
// Status commands
|
||||
lines.push("Status");
|
||||
lines.push(" /status | /whoami | /context");
|
||||
lines.push("");
|
||||
|
||||
// Skills
|
||||
lines.push("Skills");
|
||||
lines.push(" /skill <name> [input]");
|
||||
|
||||
lines.push("");
|
||||
lines.push("More: /commands for full list");
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
const COMMANDS_PER_PAGE = 8;
|
||||
|
||||
export type CommandsMessageOptions = {
|
||||
page?: number;
|
||||
surface?: string;
|
||||
};
|
||||
|
||||
export type CommandsMessageResult = {
|
||||
text: string;
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
};
|
||||
|
||||
function formatCommandEntry(command: ChatCommandDefinition): string {
|
||||
const primary = command.nativeName
|
||||
? `/${command.nativeName}`
|
||||
: command.textAliases[0]?.trim() || `/${command.key}`;
|
||||
const seen = new Set<string>();
|
||||
const aliases = command.textAliases
|
||||
.map((alias) => alias.trim())
|
||||
.filter(Boolean)
|
||||
.filter((alias) => alias.toLowerCase() !== primary.toLowerCase())
|
||||
.filter((alias) => {
|
||||
const key = alias.toLowerCase();
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
const aliasLabel = aliases.length ? ` (${aliases.join(", ")})` : "";
|
||||
const scopeLabel = command.scope === "text" ? " [text]" : "";
|
||||
return `${primary}${aliasLabel}${scopeLabel} - ${command.description}`;
|
||||
}
|
||||
|
||||
export function buildCommandsMessage(
|
||||
cfg?: ClawdbotConfig,
|
||||
skillCommands?: SkillCommandSpec[],
|
||||
options?: CommandsMessageOptions,
|
||||
): string {
|
||||
const lines = ["ℹ️ Slash commands"];
|
||||
const result = buildCommandsMessagePaginated(cfg, skillCommands, options);
|
||||
return result.text;
|
||||
}
|
||||
|
||||
export function buildCommandsMessagePaginated(
|
||||
cfg?: ClawdbotConfig,
|
||||
skillCommands?: SkillCommandSpec[],
|
||||
options?: CommandsMessageOptions,
|
||||
): CommandsMessageResult {
|
||||
const page = Math.max(1, options?.page ?? 1);
|
||||
const surface = options?.surface?.toLowerCase();
|
||||
const isTelegram = surface === "telegram";
|
||||
|
||||
const commands = cfg
|
||||
? listChatCommandsForConfig(cfg, { skillCommands })
|
||||
: listChatCommands({ skillCommands });
|
||||
for (const command of commands) {
|
||||
const primary = command.nativeName
|
||||
? `/${command.nativeName}`
|
||||
: command.textAliases[0]?.trim() || `/${command.key}`;
|
||||
const seen = new Set<string>();
|
||||
const aliases = command.textAliases
|
||||
.map((alias) => alias.trim())
|
||||
.filter(Boolean)
|
||||
.filter((alias) => alias.toLowerCase() !== primary.toLowerCase())
|
||||
.filter((alias) => {
|
||||
const key = alias.toLowerCase();
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
const aliasLabel = aliases.length ? ` (aliases: ${aliases.join(", ")})` : "";
|
||||
const scopeLabel = command.scope === "text" ? " (text-only)" : "";
|
||||
lines.push(`${primary}${aliasLabel}${scopeLabel} - ${command.description}`);
|
||||
}
|
||||
const pluginCommands = listPluginCommands();
|
||||
if (pluginCommands.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Plugin commands:");
|
||||
for (const command of pluginCommands) {
|
||||
const pluginLabel = command.pluginId ? ` (plugin: ${command.pluginId})` : "";
|
||||
lines.push(`/${command.name}${pluginLabel} - ${command.description}`);
|
||||
|
||||
// For non-Telegram surfaces, show grouped list without pagination
|
||||
if (!isTelegram) {
|
||||
const grouped = groupCommandsByCategory(commands);
|
||||
const lines = ["ℹ️ Slash commands", ""];
|
||||
|
||||
for (const category of CATEGORY_ORDER) {
|
||||
const categoryCommands = grouped.get(category) ?? [];
|
||||
if (categoryCommands.length === 0) continue;
|
||||
|
||||
lines.push(`${CATEGORY_LABELS[category]}`);
|
||||
for (const command of categoryCommands) {
|
||||
lines.push(` ${formatCommandEntry(command)}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
if (pluginCommands.length > 0) {
|
||||
lines.push("Plugins");
|
||||
for (const command of pluginCommands) {
|
||||
const pluginLabel = command.pluginId ? ` (${command.pluginId})` : "";
|
||||
lines.push(` /${command.name}${pluginLabel} - ${command.description}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
text: lines.join("\n").trim(),
|
||||
totalPages: 1,
|
||||
currentPage: 1,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
};
|
||||
}
|
||||
|
||||
// For Telegram, use pagination
|
||||
const grouped = groupCommandsByCategory(commands);
|
||||
|
||||
// Flatten commands with category headers for pagination
|
||||
type PageItem =
|
||||
| { type: "header"; category: CommandCategory }
|
||||
| { type: "command"; command: ChatCommandDefinition };
|
||||
const items: PageItem[] = [];
|
||||
|
||||
for (const category of CATEGORY_ORDER) {
|
||||
const categoryCommands = grouped.get(category) ?? [];
|
||||
if (categoryCommands.length === 0) continue;
|
||||
items.push({ type: "header", category });
|
||||
for (const command of categoryCommands) {
|
||||
items.push({ type: "command", command });
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
|
||||
// Add plugin commands
|
||||
if (pluginCommands.length > 0) {
|
||||
items.push({ type: "header", category: "tools" }); // Reuse tools category for plugins header indicator
|
||||
}
|
||||
|
||||
// Calculate pages based on command count (headers don't count toward limit)
|
||||
const commandItems = items.filter((item) => item.type === "command");
|
||||
const totalCommands = commandItems.length + pluginCommands.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalCommands / COMMANDS_PER_PAGE));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const startIndex = (currentPage - 1) * COMMANDS_PER_PAGE;
|
||||
const endIndex = startIndex + COMMANDS_PER_PAGE;
|
||||
|
||||
// Build page content
|
||||
const lines = [`ℹ️ Commands (${currentPage}/${totalPages})`, ""];
|
||||
|
||||
let commandIndex = 0;
|
||||
let currentCategory: CommandCategory | null = null;
|
||||
let pageCommandCount = 0;
|
||||
|
||||
for (const item of items) {
|
||||
if (pageCommandCount >= COMMANDS_PER_PAGE) break;
|
||||
|
||||
if (item.type === "header") {
|
||||
currentCategory = item.category;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (commandIndex >= startIndex && commandIndex < endIndex) {
|
||||
// Add category header if this is the first command of a category on this page
|
||||
if (
|
||||
(currentCategory && pageCommandCount === 0) ||
|
||||
items[items.indexOf(item) - 1]?.type === "header"
|
||||
) {
|
||||
if (currentCategory) {
|
||||
if (pageCommandCount > 0) lines.push("");
|
||||
lines.push(CATEGORY_LABELS[currentCategory]);
|
||||
}
|
||||
}
|
||||
lines.push(` ${formatCommandEntry(item.command)}`);
|
||||
pageCommandCount++;
|
||||
}
|
||||
commandIndex++;
|
||||
}
|
||||
|
||||
// Add plugin commands if they fall within this page range
|
||||
const pluginStartIndex = commandItems.length;
|
||||
for (let i = 0; i < pluginCommands.length && pageCommandCount < COMMANDS_PER_PAGE; i++) {
|
||||
const pluginIndex = pluginStartIndex + i;
|
||||
if (pluginIndex >= startIndex && pluginIndex < endIndex) {
|
||||
if (i === 0 || pluginIndex === startIndex) {
|
||||
if (pageCommandCount > 0) lines.push("");
|
||||
lines.push("Plugins");
|
||||
}
|
||||
const command = pluginCommands[i];
|
||||
const pluginLabel = command.pluginId ? ` (${command.pluginId})` : "";
|
||||
lines.push(` /${command.name}${pluginLabel} - ${command.description}`);
|
||||
pageCommandCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
text: lines.join("\n"),
|
||||
totalPages,
|
||||
currentPage,
|
||||
hasNext: currentPage < totalPages,
|
||||
hasPrev: currentPage > 1,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user