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
@@ -2,7 +2,11 @@ import { listChannelDocks } from "../channels/dock.js";
|
|||||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||||
import { listThinkingLevels } from "./thinking.js";
|
import { listThinkingLevels } from "./thinking.js";
|
||||||
import { COMMAND_ARG_FORMATTERS } from "./commands-args.js";
|
import { COMMAND_ARG_FORMATTERS } from "./commands-args.js";
|
||||||
import type { ChatCommandDefinition, CommandScope } from "./commands-registry.types.js";
|
import type {
|
||||||
|
ChatCommandDefinition,
|
||||||
|
CommandCategory,
|
||||||
|
CommandScope,
|
||||||
|
} from "./commands-registry.types.js";
|
||||||
|
|
||||||
type DefineChatCommandInput = {
|
type DefineChatCommandInput = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -16,6 +20,7 @@ type DefineChatCommandInput = {
|
|||||||
textAlias?: string;
|
textAlias?: string;
|
||||||
textAliases?: string[];
|
textAliases?: string[];
|
||||||
scope?: CommandScope;
|
scope?: CommandScope;
|
||||||
|
category?: CommandCategory;
|
||||||
};
|
};
|
||||||
|
|
||||||
function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefinition {
|
function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefinition {
|
||||||
@@ -37,6 +42,7 @@ function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefiniti
|
|||||||
argsMenu: command.argsMenu,
|
argsMenu: command.argsMenu,
|
||||||
textAliases: aliases,
|
textAliases: aliases,
|
||||||
scope,
|
scope,
|
||||||
|
category: command.category,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +54,7 @@ function defineDockCommand(dock: ChannelDock): ChatCommandDefinition {
|
|||||||
nativeName: `dock_${dock.id}`,
|
nativeName: `dock_${dock.id}`,
|
||||||
description: `Switch to ${dock.id} for replies.`,
|
description: `Switch to ${dock.id} for replies.`,
|
||||||
textAliases: [`/dock-${dock.id}`, `/dock_${dock.id}`],
|
textAliases: [`/dock-${dock.id}`, `/dock_${dock.id}`],
|
||||||
|
category: "docks",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,18 +131,21 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "help",
|
nativeName: "help",
|
||||||
description: "Show available commands.",
|
description: "Show available commands.",
|
||||||
textAlias: "/help",
|
textAlias: "/help",
|
||||||
|
category: "status",
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "commands",
|
key: "commands",
|
||||||
nativeName: "commands",
|
nativeName: "commands",
|
||||||
description: "List all slash commands.",
|
description: "List all slash commands.",
|
||||||
textAlias: "/commands",
|
textAlias: "/commands",
|
||||||
|
category: "status",
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "skill",
|
key: "skill",
|
||||||
nativeName: "skill",
|
nativeName: "skill",
|
||||||
description: "Run a skill by name.",
|
description: "Run a skill by name.",
|
||||||
textAlias: "/skill",
|
textAlias: "/skill",
|
||||||
|
category: "tools",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "name",
|
name: "name",
|
||||||
@@ -156,6 +166,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "status",
|
nativeName: "status",
|
||||||
description: "Show current status.",
|
description: "Show current status.",
|
||||||
textAlias: "/status",
|
textAlias: "/status",
|
||||||
|
category: "status",
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "allowlist",
|
key: "allowlist",
|
||||||
@@ -163,6 +174,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
textAlias: "/allowlist",
|
textAlias: "/allowlist",
|
||||||
acceptsArgs: true,
|
acceptsArgs: true,
|
||||||
scope: "text",
|
scope: "text",
|
||||||
|
category: "management",
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "approve",
|
key: "approve",
|
||||||
@@ -170,6 +182,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
description: "Approve or deny exec requests.",
|
description: "Approve or deny exec requests.",
|
||||||
textAlias: "/approve",
|
textAlias: "/approve",
|
||||||
acceptsArgs: true,
|
acceptsArgs: true,
|
||||||
|
category: "management",
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "context",
|
key: "context",
|
||||||
@@ -177,12 +190,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
description: "Explain how context is built and used.",
|
description: "Explain how context is built and used.",
|
||||||
textAlias: "/context",
|
textAlias: "/context",
|
||||||
acceptsArgs: true,
|
acceptsArgs: true,
|
||||||
|
category: "status",
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "tts",
|
key: "tts",
|
||||||
nativeName: "tts",
|
nativeName: "tts",
|
||||||
description: "Control text-to-speech (TTS).",
|
description: "Control text-to-speech (TTS).",
|
||||||
textAlias: "/tts",
|
textAlias: "/tts",
|
||||||
|
category: "media",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "action",
|
name: "action",
|
||||||
@@ -225,12 +240,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "whoami",
|
nativeName: "whoami",
|
||||||
description: "Show your sender id.",
|
description: "Show your sender id.",
|
||||||
textAlias: "/whoami",
|
textAlias: "/whoami",
|
||||||
|
category: "status",
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "subagents",
|
key: "subagents",
|
||||||
nativeName: "subagents",
|
nativeName: "subagents",
|
||||||
description: "List/stop/log/info subagent runs for this session.",
|
description: "List/stop/log/info subagent runs for this session.",
|
||||||
textAlias: "/subagents",
|
textAlias: "/subagents",
|
||||||
|
category: "management",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "action",
|
name: "action",
|
||||||
@@ -257,6 +274,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "config",
|
nativeName: "config",
|
||||||
description: "Show or set config values.",
|
description: "Show or set config values.",
|
||||||
textAlias: "/config",
|
textAlias: "/config",
|
||||||
|
category: "management",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "action",
|
name: "action",
|
||||||
@@ -284,6 +302,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "debug",
|
nativeName: "debug",
|
||||||
description: "Set runtime debug overrides.",
|
description: "Set runtime debug overrides.",
|
||||||
textAlias: "/debug",
|
textAlias: "/debug",
|
||||||
|
category: "management",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "action",
|
name: "action",
|
||||||
@@ -311,6 +330,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "usage",
|
nativeName: "usage",
|
||||||
description: "Usage footer or cost summary.",
|
description: "Usage footer or cost summary.",
|
||||||
textAlias: "/usage",
|
textAlias: "/usage",
|
||||||
|
category: "options",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "mode",
|
name: "mode",
|
||||||
@@ -326,18 +346,21 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "stop",
|
nativeName: "stop",
|
||||||
description: "Stop the current run.",
|
description: "Stop the current run.",
|
||||||
textAlias: "/stop",
|
textAlias: "/stop",
|
||||||
|
category: "session",
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "restart",
|
key: "restart",
|
||||||
nativeName: "restart",
|
nativeName: "restart",
|
||||||
description: "Restart Clawdbot.",
|
description: "Restart Clawdbot.",
|
||||||
textAlias: "/restart",
|
textAlias: "/restart",
|
||||||
|
category: "tools",
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "activation",
|
key: "activation",
|
||||||
nativeName: "activation",
|
nativeName: "activation",
|
||||||
description: "Set group activation mode.",
|
description: "Set group activation mode.",
|
||||||
textAlias: "/activation",
|
textAlias: "/activation",
|
||||||
|
category: "management",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "mode",
|
name: "mode",
|
||||||
@@ -353,6 +376,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "send",
|
nativeName: "send",
|
||||||
description: "Set send policy.",
|
description: "Set send policy.",
|
||||||
textAlias: "/send",
|
textAlias: "/send",
|
||||||
|
category: "management",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "mode",
|
name: "mode",
|
||||||
@@ -369,6 +393,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
description: "Reset the current session.",
|
description: "Reset the current session.",
|
||||||
textAlias: "/reset",
|
textAlias: "/reset",
|
||||||
acceptsArgs: true,
|
acceptsArgs: true,
|
||||||
|
category: "session",
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "new",
|
key: "new",
|
||||||
@@ -376,12 +401,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
description: "Start a new session.",
|
description: "Start a new session.",
|
||||||
textAlias: "/new",
|
textAlias: "/new",
|
||||||
acceptsArgs: true,
|
acceptsArgs: true,
|
||||||
|
category: "session",
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "compact",
|
key: "compact",
|
||||||
description: "Compact the session context.",
|
description: "Compact the session context.",
|
||||||
textAlias: "/compact",
|
textAlias: "/compact",
|
||||||
scope: "text",
|
scope: "text",
|
||||||
|
category: "session",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "instructions",
|
name: "instructions",
|
||||||
@@ -396,6 +423,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "think",
|
nativeName: "think",
|
||||||
description: "Set thinking level.",
|
description: "Set thinking level.",
|
||||||
textAlias: "/think",
|
textAlias: "/think",
|
||||||
|
category: "options",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "level",
|
name: "level",
|
||||||
@@ -411,6 +439,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "verbose",
|
nativeName: "verbose",
|
||||||
description: "Toggle verbose mode.",
|
description: "Toggle verbose mode.",
|
||||||
textAlias: "/verbose",
|
textAlias: "/verbose",
|
||||||
|
category: "options",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "mode",
|
name: "mode",
|
||||||
@@ -426,6 +455,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "reasoning",
|
nativeName: "reasoning",
|
||||||
description: "Toggle reasoning visibility.",
|
description: "Toggle reasoning visibility.",
|
||||||
textAlias: "/reasoning",
|
textAlias: "/reasoning",
|
||||||
|
category: "options",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "mode",
|
name: "mode",
|
||||||
@@ -441,6 +471,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "elevated",
|
nativeName: "elevated",
|
||||||
description: "Toggle elevated mode.",
|
description: "Toggle elevated mode.",
|
||||||
textAlias: "/elevated",
|
textAlias: "/elevated",
|
||||||
|
category: "options",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "mode",
|
name: "mode",
|
||||||
@@ -456,6 +487,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "exec",
|
nativeName: "exec",
|
||||||
description: "Set exec defaults for this session.",
|
description: "Set exec defaults for this session.",
|
||||||
textAlias: "/exec",
|
textAlias: "/exec",
|
||||||
|
category: "options",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "options",
|
name: "options",
|
||||||
@@ -470,6 +502,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "model",
|
nativeName: "model",
|
||||||
description: "Show or set the model.",
|
description: "Show or set the model.",
|
||||||
textAlias: "/model",
|
textAlias: "/model",
|
||||||
|
category: "options",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "model",
|
name: "model",
|
||||||
@@ -485,12 +518,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
textAlias: "/models",
|
textAlias: "/models",
|
||||||
argsParsing: "none",
|
argsParsing: "none",
|
||||||
acceptsArgs: true,
|
acceptsArgs: true,
|
||||||
|
category: "options",
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "queue",
|
key: "queue",
|
||||||
nativeName: "queue",
|
nativeName: "queue",
|
||||||
description: "Adjust queue settings.",
|
description: "Adjust queue settings.",
|
||||||
textAlias: "/queue",
|
textAlias: "/queue",
|
||||||
|
category: "options",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "mode",
|
name: "mode",
|
||||||
@@ -523,6 +558,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
description: "Run host shell commands (host-only).",
|
description: "Run host shell commands (host-only).",
|
||||||
textAlias: "/bash",
|
textAlias: "/bash",
|
||||||
scope: "text",
|
scope: "text",
|
||||||
|
category: "tools",
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "command",
|
name: "command",
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ import type { ClawdbotConfig } from "../config/types.js";
|
|||||||
|
|
||||||
export type CommandScope = "text" | "native" | "both";
|
export type CommandScope = "text" | "native" | "both";
|
||||||
|
|
||||||
|
export type CommandCategory =
|
||||||
|
| "session"
|
||||||
|
| "options"
|
||||||
|
| "status"
|
||||||
|
| "management"
|
||||||
|
| "media"
|
||||||
|
| "tools"
|
||||||
|
| "docks";
|
||||||
|
|
||||||
export type CommandArgType = "string" | "number" | "boolean";
|
export type CommandArgType = "string" | "number" | "boolean";
|
||||||
|
|
||||||
export type CommandArgChoiceContext = {
|
export type CommandArgChoiceContext = {
|
||||||
@@ -51,6 +60,7 @@ export type ChatCommandDefinition = {
|
|||||||
formatArgs?: (values: CommandArgValues) => string | undefined;
|
formatArgs?: (values: CommandArgValues) => string | undefined;
|
||||||
argsMenu?: CommandArgMenuSpec | "auto";
|
argsMenu?: CommandArgMenuSpec | "auto";
|
||||||
scope: CommandScope;
|
scope: CommandScope;
|
||||||
|
category?: CommandCategory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NativeCommandSpec = {
|
export type NativeCommandSpec = {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { logVerbose } from "../../globals.js";
|
import { logVerbose } from "../../globals.js";
|
||||||
import { listSkillCommandsForWorkspace } from "../skill-commands.js";
|
import { listSkillCommandsForWorkspace } from "../skill-commands.js";
|
||||||
import { buildCommandsMessage, buildHelpMessage } from "../status.js";
|
import {
|
||||||
|
buildCommandsMessage,
|
||||||
|
buildCommandsMessagePaginated,
|
||||||
|
buildHelpMessage,
|
||||||
|
} from "../status.js";
|
||||||
import { buildStatusReply } from "./commands-status.js";
|
import { buildStatusReply } from "./commands-status.js";
|
||||||
import { buildContextReply } from "./commands-context-report.js";
|
import { buildContextReply } from "./commands-context-report.js";
|
||||||
import type { CommandHandler } from "./commands-types.js";
|
import type { CommandHandler } from "./commands-types.js";
|
||||||
@@ -35,12 +39,61 @@ export const handleCommandsListCommand: CommandHandler = async (params, allowTex
|
|||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
});
|
});
|
||||||
|
const surface = params.ctx.Surface;
|
||||||
|
|
||||||
|
// For Telegram, return paginated result with inline buttons
|
||||||
|
if (surface === "telegram") {
|
||||||
|
const result = buildCommandsMessagePaginated(params.cfg, skillCommands, {
|
||||||
|
page: 1,
|
||||||
|
surface,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build inline keyboard for pagination if there are multiple pages
|
||||||
|
if (result.totalPages > 1) {
|
||||||
return {
|
return {
|
||||||
shouldContinue: false,
|
shouldContinue: false,
|
||||||
reply: { text: buildCommandsMessage(params.cfg, skillCommands) },
|
reply: {
|
||||||
|
text: result.text,
|
||||||
|
channelData: {
|
||||||
|
telegram: {
|
||||||
|
buttons: buildCommandsPaginationKeyboard(result.currentPage, result.totalPages),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldContinue: false,
|
||||||
|
reply: { text: result.text },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldContinue: false,
|
||||||
|
reply: { text: buildCommandsMessage(params.cfg, skillCommands, { surface }) },
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function buildCommandsPaginationKeyboard(
|
||||||
|
currentPage: number,
|
||||||
|
totalPages: number,
|
||||||
|
): Array<Array<{ text: string; callback_data: string }>> {
|
||||||
|
const buttons: Array<{ text: string; callback_data: string }> = [];
|
||||||
|
|
||||||
|
if (currentPage > 1) {
|
||||||
|
buttons.push({ text: "◀ Prev", callback_data: `commands_page_${currentPage - 1}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push({ text: `${currentPage}/${totalPages}`, callback_data: "commands_page_noop" });
|
||||||
|
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
buttons.push({ text: "Next ▶", callback_data: `commands_page_${currentPage + 1}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return [buttons];
|
||||||
|
}
|
||||||
|
|
||||||
export const handleStatusCommand: CommandHandler = async (params, allowTextCommands) => {
|
export const handleStatusCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||||
if (!allowTextCommands) return null;
|
if (!allowTextCommands) return null;
|
||||||
const statusRequested =
|
const statusRequested =
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ import {
|
|||||||
resolveModelCostConfig,
|
resolveModelCostConfig,
|
||||||
} from "../utils/usage-format.js";
|
} from "../utils/usage-format.js";
|
||||||
import { VERSION } from "../version.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 { listPluginCommands } from "../plugins/commands.js";
|
||||||
import type { SkillCommandSpec } from "../agents/skills.js";
|
import type { SkillCommandSpec } from "../agents/skills.js";
|
||||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js";
|
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js";
|
||||||
@@ -427,35 +432,89 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildHelpMessage(cfg?: ClawdbotConfig): string {
|
const CATEGORY_LABELS: Record<CommandCategory, string> = {
|
||||||
const options = [
|
session: "Session",
|
||||||
"/think <level>",
|
options: "Options",
|
||||||
"/verbose on|full|off",
|
status: "Status",
|
||||||
"/reasoning on|off",
|
management: "Management",
|
||||||
"/elevated on|off|ask|full",
|
media: "Media",
|
||||||
"/model <id>",
|
tools: "Tools",
|
||||||
"/usage off|tokens|full",
|
docks: "Docks",
|
||||||
];
|
};
|
||||||
if (cfg?.commands?.config === true) options.push("/config show");
|
|
||||||
if (cfg?.commands?.debug === true) options.push("/debug show");
|
const CATEGORY_ORDER: CommandCategory[] = [
|
||||||
return [
|
"session",
|
||||||
"ℹ️ Help",
|
"options",
|
||||||
"Shortcuts: /new reset | /compact [instructions] | /restart relink (if enabled)",
|
"status",
|
||||||
`Options: ${options.join(" | ")}`,
|
"management",
|
||||||
"Skills: /skill <name> [input]",
|
"media",
|
||||||
"More: /commands for all slash commands",
|
"tools",
|
||||||
].join("\n");
|
"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 buildCommandsMessage(
|
export function buildHelpMessage(cfg?: ClawdbotConfig): string {
|
||||||
cfg?: ClawdbotConfig,
|
const lines = ["ℹ️ Help", ""];
|
||||||
skillCommands?: SkillCommandSpec[],
|
|
||||||
): string {
|
// Session commands - quick shortcuts
|
||||||
const lines = ["ℹ️ Slash commands"];
|
lines.push("Session");
|
||||||
const commands = cfg
|
lines.push(" /new | /reset | /compact [instructions] | /stop");
|
||||||
? listChatCommandsForConfig(cfg, { skillCommands })
|
lines.push("");
|
||||||
: listChatCommands({ skillCommands });
|
|
||||||
for (const command of commands) {
|
// 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
|
const primary = command.nativeName
|
||||||
? `/${command.nativeName}`
|
? `/${command.nativeName}`
|
||||||
: command.textAliases[0]?.trim() || `/${command.key}`;
|
: command.textAliases[0]?.trim() || `/${command.key}`;
|
||||||
@@ -470,18 +529,151 @@ export function buildCommandsMessage(
|
|||||||
seen.add(key);
|
seen.add(key);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
const aliasLabel = aliases.length ? ` (aliases: ${aliases.join(", ")})` : "";
|
const aliasLabel = aliases.length ? ` (${aliases.join(", ")})` : "";
|
||||||
const scopeLabel = command.scope === "text" ? " (text-only)" : "";
|
const scopeLabel = command.scope === "text" ? " [text]" : "";
|
||||||
lines.push(`${primary}${aliasLabel}${scopeLabel} - ${command.description}`);
|
return `${primary}${aliasLabel}${scopeLabel} - ${command.description}`;
|
||||||
}
|
}
|
||||||
const pluginCommands = listPluginCommands();
|
|
||||||
if (pluginCommands.length > 0) {
|
export function buildCommandsMessage(
|
||||||
lines.push("");
|
cfg?: ClawdbotConfig,
|
||||||
lines.push("Plugin commands:");
|
skillCommands?: SkillCommandSpec[],
|
||||||
for (const command of pluginCommands) {
|
options?: CommandsMessageOptions,
|
||||||
const pluginLabel = command.pluginId ? ` (plugin: ${command.pluginId})` : "";
|
): string {
|
||||||
lines.push(`/${command.name}${pluginLabel} - ${command.description}`);
|
const result = buildCommandsMessagePaginated(cfg, skillCommands, options);
|
||||||
}
|
return result.text;
|
||||||
}
|
}
|
||||||
return lines.join("\n");
|
|
||||||
|
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 });
|
||||||
|
const pluginCommands = listPluginCommands();
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import {
|
|||||||
createInboundDebouncer,
|
createInboundDebouncer,
|
||||||
resolveInboundDebounceMs,
|
resolveInboundDebounceMs,
|
||||||
} from "../auto-reply/inbound-debounce.js";
|
} from "../auto-reply/inbound-debounce.js";
|
||||||
|
import { buildCommandsPaginationKeyboard } from "../auto-reply/reply/commands-info.js";
|
||||||
|
import { buildCommandsMessagePaginated } from "../auto-reply/status.js";
|
||||||
|
import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { writeConfigFile } from "../config/io.js";
|
import { writeConfigFile } from "../config/io.js";
|
||||||
import { danger, logVerbose, warn } from "../globals.js";
|
import { danger, logVerbose, warn } from "../globals.js";
|
||||||
@@ -17,6 +20,7 @@ import { migrateTelegramGroupConfig } from "./group-migration.js";
|
|||||||
import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
|
import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
|
||||||
import { readTelegramAllowFromStore } from "./pairing-store.js";
|
import { readTelegramAllowFromStore } from "./pairing-store.js";
|
||||||
import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js";
|
import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js";
|
||||||
|
import { buildInlineKeyboard } from "./send.js";
|
||||||
|
|
||||||
export const registerTelegramHandlers = ({
|
export const registerTelegramHandlers = ({
|
||||||
cfg,
|
cfg,
|
||||||
@@ -199,6 +203,47 @@ export const registerTelegramHandlers = ({
|
|||||||
const callbackMessage = callback.message;
|
const callbackMessage = callback.message;
|
||||||
if (!data || !callbackMessage) return;
|
if (!data || !callbackMessage) return;
|
||||||
|
|
||||||
|
// Handle commands pagination callback
|
||||||
|
const paginationMatch = data.match(/^commands_page_(\d+|noop)$/);
|
||||||
|
if (paginationMatch) {
|
||||||
|
const pageValue = paginationMatch[1];
|
||||||
|
if (pageValue === "noop") return; // Page number button - no action
|
||||||
|
|
||||||
|
const page = parseInt(pageValue, 10);
|
||||||
|
if (isNaN(page) || page < 1) return;
|
||||||
|
|
||||||
|
const skillCommands = listSkillCommandsForAgents({ cfg });
|
||||||
|
const result = buildCommandsMessagePaginated(cfg, skillCommands, {
|
||||||
|
page,
|
||||||
|
surface: "telegram",
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageId = callbackMessage.message_id;
|
||||||
|
const chatId = callbackMessage.chat.id;
|
||||||
|
const keyboard =
|
||||||
|
result.totalPages > 1
|
||||||
|
? buildInlineKeyboard(
|
||||||
|
buildCommandsPaginationKeyboard(result.currentPage, result.totalPages),
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bot.api.editMessageText(
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
result.text,
|
||||||
|
keyboard ? { reply_markup: keyboard } : undefined,
|
||||||
|
);
|
||||||
|
} catch (editErr) {
|
||||||
|
// Ignore "message is not modified" errors (user clicked same page)
|
||||||
|
const errStr = String(editErr);
|
||||||
|
if (!errStr.includes("message is not modified")) {
|
||||||
|
throw editErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
||||||
cfg,
|
cfg,
|
||||||
accountId,
|
accountId,
|
||||||
|
|||||||
Reference in New Issue
Block a user