import { getChannelDock } from "../../channels/dock.js"; import type { ClawdbotConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; import type { MsgContext, TemplateContext } from "../templating.js"; import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; import { getAbortMemory } from "./abort.js"; import { buildStatusReply, handleCommands } from "./commands.js"; import type { InlineDirectives } from "./directive-handling.js"; import { isDirectiveOnly } from "./directive-handling.js"; import type { createModelSelectionState } from "./model-selection.js"; import { extractInlineSimpleCommand } from "./reply-inline.js"; import type { TypingController } from "./typing.js"; import { listSkillCommandsForWorkspace, resolveSkillCommandInvocation } from "../skill-commands.js"; import { logVerbose } from "../../globals.js"; export type InlineActionResult = | { kind: "reply"; reply: ReplyPayload | ReplyPayload[] | undefined } | { kind: "continue"; directives: InlineDirectives; abortedLastRun: boolean; }; export async function handleInlineActions(params: { ctx: MsgContext; sessionCtx: TemplateContext; cfg: ClawdbotConfig; agentId: string; sessionEntry?: SessionEntry; sessionStore?: Record; sessionKey: string; storePath?: string; sessionScope: Parameters[0]["sessionScope"]; workspaceDir: string; isGroup: boolean; opts?: GetReplyOptions; typing: TypingController; allowTextCommands: boolean; inlineStatusRequested: boolean; command: Parameters[0]["command"]; directives: InlineDirectives; cleanedBody: string; elevatedEnabled: boolean; elevatedAllowed: boolean; elevatedFailures: Array<{ gate: string; key: string }>; defaultActivation: Parameters[0]["defaultGroupActivation"]; resolvedThinkLevel: ThinkLevel | undefined; resolvedVerboseLevel: VerboseLevel | undefined; resolvedReasoningLevel: ReasoningLevel; resolvedElevatedLevel: ElevatedLevel; resolveDefaultThinkingLevel: Awaited< ReturnType >["resolveDefaultThinkingLevel"]; provider: string; model: string; contextTokens: number; directiveAck?: ReplyPayload; abortedLastRun: boolean; skillFilter?: string[]; }): Promise { const { ctx, sessionCtx, cfg, agentId, sessionEntry, sessionStore, sessionKey, storePath, sessionScope, workspaceDir, isGroup, opts, typing, allowTextCommands, inlineStatusRequested, command, directives: initialDirectives, cleanedBody: initialCleanedBody, elevatedEnabled, elevatedAllowed, elevatedFailures, defaultActivation, resolvedThinkLevel, resolvedVerboseLevel, resolvedReasoningLevel, resolvedElevatedLevel, resolveDefaultThinkingLevel, provider, model, contextTokens, directiveAck, abortedLastRun: initialAbortedLastRun, skillFilter, } = params; let directives = initialDirectives; let cleanedBody = initialCleanedBody; const shouldLoadSkillCommands = command.commandBodyNormalized.startsWith("/"); const skillCommands = shouldLoadSkillCommands ? listSkillCommandsForWorkspace({ workspaceDir, cfg, skillFilter, }) : []; const skillInvocation = allowTextCommands && skillCommands.length > 0 ? resolveSkillCommandInvocation({ commandBodyNormalized: command.commandBodyNormalized, skillCommands, }) : null; if (skillInvocation) { if (!command.isAuthorizedSender) { logVerbose( `Ignoring /${skillInvocation.command.name} from unauthorized sender: ${command.senderId || ""}`, ); typing.cleanup(); return { kind: "reply", reply: undefined }; } const promptParts = [ `Use the "${skillInvocation.command.skillName}" skill for this request.`, skillInvocation.args ? `User input:\n${skillInvocation.args}` : null, ].filter((entry): entry is string => Boolean(entry)); const rewrittenBody = promptParts.join("\n\n"); ctx.Body = rewrittenBody; sessionCtx.Body = rewrittenBody; sessionCtx.BodyStripped = rewrittenBody; cleanedBody = rewrittenBody; } const sendInlineReply = async (reply?: ReplyPayload) => { if (!reply) return; if (!opts?.onBlockReply) return; await opts.onBlockReply(reply); }; const inlineCommand = allowTextCommands && command.isAuthorizedSender ? extractInlineSimpleCommand(cleanedBody) : null; if (inlineCommand) { cleanedBody = inlineCommand.cleaned; sessionCtx.Body = cleanedBody; sessionCtx.BodyStripped = cleanedBody; } const handleInlineStatus = !isDirectiveOnly({ directives, cleanedBody: directives.cleaned, ctx, cfg, agentId, isGroup, }) && inlineStatusRequested; if (handleInlineStatus) { const inlineStatusReply = await buildStatusReply({ cfg, command, sessionEntry, sessionKey, sessionScope, provider, model, contextTokens, resolvedThinkLevel, resolvedVerboseLevel: resolvedVerboseLevel ?? "off", resolvedReasoningLevel, resolvedElevatedLevel, resolveDefaultThinkingLevel, isGroup, defaultGroupActivation: defaultActivation, }); await sendInlineReply(inlineStatusReply); directives = { ...directives, hasStatusDirective: false }; } if (inlineCommand) { const inlineCommandContext = { ...command, rawBodyNormalized: inlineCommand.command, commandBodyNormalized: inlineCommand.command, }; const inlineResult = await handleCommands({ ctx, cfg, command: inlineCommandContext, agentId, directives, elevated: { enabled: elevatedEnabled, allowed: elevatedAllowed, failures: elevatedFailures, }, sessionEntry, sessionStore, sessionKey, storePath, sessionScope, workspaceDir, defaultGroupActivation: defaultActivation, resolvedThinkLevel, resolvedVerboseLevel: resolvedVerboseLevel ?? "off", resolvedReasoningLevel, resolvedElevatedLevel, resolveDefaultThinkingLevel, provider, model, contextTokens, isGroup, skillCommands, }); if (inlineResult.reply) { if (!inlineCommand.cleaned) { typing.cleanup(); return { kind: "reply", reply: inlineResult.reply }; } await sendInlineReply(inlineResult.reply); } } if (directiveAck) { await sendInlineReply(directiveAck); } const isEmptyConfig = Object.keys(cfg).length === 0; const skipWhenConfigEmpty = command.channelId ? Boolean(getChannelDock(command.channelId)?.commands?.skipWhenConfigEmpty) : false; if ( skipWhenConfigEmpty && isEmptyConfig && command.from && command.to && command.from !== command.to ) { typing.cleanup(); return { kind: "reply", reply: undefined }; } let abortedLastRun = initialAbortedLastRun; if (!sessionEntry && command.abortKey) { abortedLastRun = getAbortMemory(command.abortKey) ?? false; } const commandResult = await handleCommands({ ctx, cfg, command, agentId, directives, elevated: { enabled: elevatedEnabled, allowed: elevatedAllowed, failures: elevatedFailures, }, sessionEntry, sessionStore, sessionKey, storePath, sessionScope, workspaceDir, defaultGroupActivation: defaultActivation, resolvedThinkLevel, resolvedVerboseLevel: resolvedVerboseLevel ?? "off", resolvedReasoningLevel, resolvedElevatedLevel, resolveDefaultThinkingLevel, provider, model, contextTokens, isGroup, skillCommands, }); if (!commandResult.shouldContinue) { typing.cleanup(); return { kind: "reply", reply: commandResult.reply }; } return { kind: "continue", directives, abortedLastRun, }; }