2025-12-07 16:53:19 +00:00
|
|
|
|
import fs from "node:fs";
|
|
|
|
|
|
import { lookupContextTokens } from "../agents/context.js";
|
2026-01-14 14:31:43 +00:00
|
|
|
|
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
2026-01-09 02:21:17 +00:00
|
|
|
|
import { resolveModelAuthMode } from "../agents/model-auth.js";
|
2025-12-26 00:16:29 +01:00
|
|
|
|
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
2026-01-12 01:23:37 +00:00
|
|
|
|
import { resolveSandboxRuntimeStatus } from "../agents/sandbox.js";
|
2026-02-18 01:34:35 +00:00
|
|
|
|
import type { SkillCommandSpec } from "../agents/skills.js";
|
2026-01-14 14:31:43 +00:00
|
|
|
|
import { derivePromptTokens, normalizeUsage, type UsageLike } from "../agents/usage.js";
|
2026-02-18 01:34:35 +00:00
|
|
|
|
import type { OpenClawConfig } from "../config/config.js";
|
2025-12-17 11:29:04 +01:00
|
|
|
|
import {
|
2026-01-09 03:14:39 +00:00
|
|
|
|
resolveMainSessionKey,
|
2026-01-07 09:02:20 -06:00
|
|
|
|
resolveSessionFilePath,
|
2026-02-12 23:23:12 -05:00
|
|
|
|
resolveSessionFilePathOptions,
|
2025-12-17 11:29:04 +01:00
|
|
|
|
type SessionEntry,
|
|
|
|
|
|
type SessionScope,
|
|
|
|
|
|
} from "../config/sessions.js";
|
2026-02-08 04:53:31 -08:00
|
|
|
|
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
2026-02-01 10:03:47 +09:00
|
|
|
|
import { resolveCommitHash } from "../infra/git-commit.js";
|
2026-02-18 01:34:35 +00:00
|
|
|
|
import type { MediaUnderstandingDecision } from "../media-understanding/types.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
|
import { listPluginCommands } from "../plugins/commands.js";
|
2026-02-12 23:23:12 -05:00
|
|
|
|
import { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
2026-01-24 10:03:19 +00:00
|
|
|
|
import {
|
|
|
|
|
|
getTtsMaxLength,
|
|
|
|
|
|
getTtsProvider,
|
|
|
|
|
|
isSummarizationEnabled,
|
2026-01-24 23:35:20 -05:00
|
|
|
|
resolveTtsAutoMode,
|
2026-01-24 10:03:19 +00:00
|
|
|
|
resolveTtsConfig,
|
|
|
|
|
|
resolveTtsPrefsPath,
|
|
|
|
|
|
} from "../tts/tts.js";
|
2026-01-09 02:21:17 +00:00
|
|
|
|
import {
|
|
|
|
|
|
estimateUsageCost,
|
|
|
|
|
|
formatTokenCount as formatTokenCountShared,
|
|
|
|
|
|
formatUsd,
|
|
|
|
|
|
resolveModelCostConfig,
|
|
|
|
|
|
} from "../utils/usage-format.js";
|
2026-01-08 22:51:35 -05:00
|
|
|
|
import { VERSION } from "../version.js";
|
2026-01-27 09:37:22 +08:00
|
|
|
|
import {
|
|
|
|
|
|
listChatCommands,
|
|
|
|
|
|
listChatCommandsForConfig,
|
|
|
|
|
|
type ChatCommandDefinition,
|
|
|
|
|
|
} from "./commands-registry.js";
|
2026-02-18 01:34:35 +00:00
|
|
|
|
import type { CommandCategory } from "./commands-registry.types.js";
|
2026-02-19 14:33:02 -08:00
|
|
|
|
import { resolveActiveFallbackState } from "./fallback-state.js";
|
|
|
|
|
|
import { formatProviderModelRef, resolveSelectedAndActiveModel } from "./model-runtime.js";
|
2026-02-18 01:34:35 +00:00
|
|
|
|
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js";
|
2025-12-07 16:53:19 +00:00
|
|
|
|
|
2026-02-17 11:16:36 +09:00
|
|
|
|
type AgentDefaults = NonNullable<NonNullable<OpenClawConfig["agents"]>["defaults"]>;
|
|
|
|
|
|
type AgentConfig = Partial<AgentDefaults> & {
|
|
|
|
|
|
model?: AgentDefaults["model"] | string;
|
|
|
|
|
|
};
|
2025-12-07 16:53:19 +00:00
|
|
|
|
|
2026-01-09 02:21:17 +00:00
|
|
|
|
export const formatTokenCount = formatTokenCountShared;
|
|
|
|
|
|
|
2026-01-07 07:21:56 +01:00
|
|
|
|
type QueueStatus = {
|
|
|
|
|
|
mode?: string;
|
|
|
|
|
|
depth?: number;
|
|
|
|
|
|
debounceMs?: number;
|
|
|
|
|
|
cap?: number;
|
|
|
|
|
|
dropPolicy?: string;
|
|
|
|
|
|
showDetails?: boolean;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-07 16:53:19 +00:00
|
|
|
|
type StatusArgs = {
|
2026-01-30 03:15:10 +01:00
|
|
|
|
config?: OpenClawConfig;
|
2025-12-17 11:29:04 +01:00
|
|
|
|
agent: AgentConfig;
|
2026-02-13 14:17:24 +01:00
|
|
|
|
agentId?: string;
|
2025-12-07 16:53:19 +00:00
|
|
|
|
sessionEntry?: SessionEntry;
|
|
|
|
|
|
sessionKey?: string;
|
|
|
|
|
|
sessionScope?: SessionScope;
|
2026-02-12 23:23:12 -05:00
|
|
|
|
sessionStorePath?: string;
|
2026-01-02 22:23:00 +01:00
|
|
|
|
groupActivation?: "mention" | "always";
|
2025-12-07 16:53:19 +00:00
|
|
|
|
resolvedThink?: ThinkLevel;
|
|
|
|
|
|
resolvedVerbose?: VerboseLevel;
|
2026-01-07 06:16:38 +01:00
|
|
|
|
resolvedReasoning?: ReasoningLevel;
|
2026-01-05 07:07:17 +01:00
|
|
|
|
resolvedElevated?: ElevatedLevel;
|
2026-01-05 15:50:14 +01:00
|
|
|
|
modelAuth?: string;
|
2026-02-19 14:33:02 -08:00
|
|
|
|
activeModelAuth?: string;
|
2026-01-07 11:42:41 +01:00
|
|
|
|
usageLine?: string;
|
2026-01-24 06:22:54 +00:00
|
|
|
|
timeLine?: string;
|
2026-01-07 07:21:56 +01:00
|
|
|
|
queue?: QueueStatus;
|
2026-02-17 11:16:36 +09:00
|
|
|
|
mediaDecisions?: ReadonlyArray<MediaUnderstandingDecision>;
|
2026-01-18 04:44:52 +00:00
|
|
|
|
subagentsLine?: string;
|
2026-01-07 07:21:56 +01:00
|
|
|
|
includeTranscriptUsage?: boolean;
|
2026-01-09 03:14:39 +00:00
|
|
|
|
now?: number;
|
2025-12-07 16:53:19 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-21 09:07:21 +00:00
|
|
|
|
function resolveRuntimeLabel(
|
|
|
|
|
|
args: Pick<StatusArgs, "config" | "agent" | "sessionKey" | "sessionScope">,
|
|
|
|
|
|
): string {
|
|
|
|
|
|
const sessionKey = args.sessionKey?.trim();
|
|
|
|
|
|
if (args.config && sessionKey) {
|
|
|
|
|
|
const runtimeStatus = resolveSandboxRuntimeStatus({
|
|
|
|
|
|
cfg: args.config,
|
|
|
|
|
|
sessionKey,
|
|
|
|
|
|
});
|
|
|
|
|
|
const sandboxMode = runtimeStatus.mode ?? "off";
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (sandboxMode === "off") {
|
|
|
|
|
|
return "direct";
|
|
|
|
|
|
}
|
2026-01-21 09:07:21 +00:00
|
|
|
|
const runtime = runtimeStatus.sandboxed ? "docker" : sessionKey ? "direct" : "unknown";
|
|
|
|
|
|
return `${runtime}/${sandboxMode}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sandboxMode = args.agent?.sandbox?.mode ?? "off";
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (sandboxMode === "off") {
|
|
|
|
|
|
return "direct";
|
|
|
|
|
|
}
|
2026-01-21 09:07:21 +00:00
|
|
|
|
const sandboxed = (() => {
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (!sessionKey) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sandboxMode === "all") {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-01-21 09:07:21 +00:00
|
|
|
|
if (args.config) {
|
|
|
|
|
|
return resolveSandboxRuntimeStatus({
|
|
|
|
|
|
cfg: args.config,
|
|
|
|
|
|
sessionKey,
|
|
|
|
|
|
}).sandboxed;
|
|
|
|
|
|
}
|
|
|
|
|
|
const sessionScope = args.sessionScope ?? "per-sender";
|
|
|
|
|
|
const mainKey = resolveMainSessionKey({
|
|
|
|
|
|
session: { scope: sessionScope },
|
|
|
|
|
|
});
|
|
|
|
|
|
return sessionKey !== mainKey.trim();
|
|
|
|
|
|
})();
|
|
|
|
|
|
const runtime = sandboxed ? "docker" : sessionKey ? "direct" : "unknown";
|
|
|
|
|
|
return `${runtime}/${sandboxMode}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 14:31:43 +00:00
|
|
|
|
const formatTokens = (total: number | null | undefined, contextTokens: number | null) => {
|
2025-12-07 16:53:19 +00:00
|
|
|
|
const ctx = contextTokens ?? null;
|
2025-12-08 14:46:15 +00:00
|
|
|
|
if (total == null) {
|
2026-01-09 02:21:17 +00:00
|
|
|
|
const ctxLabel = ctx ? formatTokenCount(ctx) : "?";
|
|
|
|
|
|
return `?/${ctxLabel}`;
|
2025-12-08 14:46:15 +00:00
|
|
|
|
}
|
2025-12-07 16:53:19 +00:00
|
|
|
|
const pct = ctx ? Math.min(999, Math.round((total / ctx) * 100)) : null;
|
2026-01-09 02:21:17 +00:00
|
|
|
|
const totalLabel = formatTokenCount(total);
|
|
|
|
|
|
const ctxLabel = ctx ? formatTokenCount(ctx) : "?";
|
2025-12-07 16:53:19 +00:00
|
|
|
|
return `${totalLabel}/${ctxLabel}${pct !== null ? ` (${pct}%)` : ""}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-06 02:06:06 +01:00
|
|
|
|
export const formatContextUsageShort = (
|
|
|
|
|
|
total: number | null | undefined,
|
|
|
|
|
|
contextTokens: number | null | undefined,
|
|
|
|
|
|
) => `Context ${formatTokens(total, contextTokens ?? null)}`;
|
|
|
|
|
|
|
2026-01-07 07:21:56 +01:00
|
|
|
|
const formatQueueDetails = (queue?: QueueStatus) => {
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (!queue) {
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
2026-01-07 07:21:56 +01:00
|
|
|
|
const depth = typeof queue.depth === "number" ? `depth ${queue.depth}` : null;
|
|
|
|
|
|
if (!queue.showDetails) {
|
|
|
|
|
|
return depth ? ` (${depth})` : "";
|
|
|
|
|
|
}
|
|
|
|
|
|
const detailParts: string[] = [];
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (depth) {
|
|
|
|
|
|
detailParts.push(depth);
|
|
|
|
|
|
}
|
2026-01-07 07:21:56 +01:00
|
|
|
|
if (typeof queue.debounceMs === "number") {
|
|
|
|
|
|
const ms = Math.max(0, Math.round(queue.debounceMs));
|
|
|
|
|
|
const label =
|
2026-01-14 14:31:43 +00:00
|
|
|
|
ms >= 1000 ? `${ms % 1000 === 0 ? ms / 1000 : (ms / 1000).toFixed(1)}s` : `${ms}ms`;
|
2026-01-07 07:21:56 +01:00
|
|
|
|
detailParts.push(`debounce ${label}`);
|
|
|
|
|
|
}
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (typeof queue.cap === "number") {
|
|
|
|
|
|
detailParts.push(`cap ${queue.cap}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (queue.dropPolicy) {
|
|
|
|
|
|
detailParts.push(`drop ${queue.dropPolicy}`);
|
|
|
|
|
|
}
|
2026-01-07 07:21:56 +01:00
|
|
|
|
return detailParts.length ? ` (${detailParts.join(" · ")})` : "";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-08 14:46:15 +00:00
|
|
|
|
const readUsageFromSessionLog = (
|
|
|
|
|
|
sessionId?: string,
|
2026-01-07 09:02:20 -06:00
|
|
|
|
sessionEntry?: SessionEntry,
|
2026-02-13 14:17:24 +01:00
|
|
|
|
agentId?: string,
|
2026-02-12 23:23:12 -05:00
|
|
|
|
sessionKey?: string,
|
|
|
|
|
|
storePath?: string,
|
2025-12-08 14:46:15 +00:00
|
|
|
|
):
|
|
|
|
|
|
| {
|
|
|
|
|
|
input: number;
|
|
|
|
|
|
output: number;
|
2025-12-12 23:22:05 +00:00
|
|
|
|
promptTokens: number;
|
2025-12-08 14:46:15 +00:00
|
|
|
|
total: number;
|
|
|
|
|
|
model?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
| undefined => {
|
2026-01-30 03:15:10 +01:00
|
|
|
|
// Transcripts are stored at the session file path (fallback: ~/.openclaw/sessions/<SessionId>.jsonl)
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (!sessionId) {
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
2026-02-12 23:23:12 -05:00
|
|
|
|
let logPath: string;
|
|
|
|
|
|
try {
|
2026-02-13 14:17:24 +01:00
|
|
|
|
const resolvedAgentId =
|
|
|
|
|
|
agentId ?? (sessionKey ? resolveAgentIdFromSessionKey(sessionKey) : undefined);
|
2026-02-12 23:23:12 -05:00
|
|
|
|
logPath = resolveSessionFilePath(
|
|
|
|
|
|
sessionId,
|
|
|
|
|
|
sessionEntry,
|
2026-02-13 14:17:24 +01:00
|
|
|
|
resolveSessionFilePathOptions({ agentId: resolvedAgentId, storePath }),
|
2026-02-12 23:23:12 -05:00
|
|
|
|
);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (!fs.existsSync(logPath)) {
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
2025-12-08 14:46:15 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const lines = fs.readFileSync(logPath, "utf-8").split(/\n+/);
|
|
|
|
|
|
let input = 0;
|
|
|
|
|
|
let output = 0;
|
2025-12-12 23:22:05 +00:00
|
|
|
|
let promptTokens = 0;
|
2025-12-08 14:46:15 +00:00
|
|
|
|
let model: string | undefined;
|
2025-12-12 23:22:05 +00:00
|
|
|
|
let lastUsage: ReturnType<typeof normalizeUsage> | undefined;
|
2025-12-08 14:46:15 +00:00
|
|
|
|
|
|
|
|
|
|
for (const line of lines) {
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (!line.trim()) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2025-12-08 14:46:15 +00:00
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(line) as {
|
2025-12-08 17:05:27 +01:00
|
|
|
|
message?: {
|
2025-12-12 23:22:05 +00:00
|
|
|
|
usage?: UsageLike;
|
2025-12-08 17:05:27 +01:00
|
|
|
|
model?: string;
|
|
|
|
|
|
};
|
2025-12-12 23:22:05 +00:00
|
|
|
|
usage?: UsageLike;
|
2025-12-08 14:46:15 +00:00
|
|
|
|
model?: string;
|
|
|
|
|
|
};
|
2025-12-12 23:22:05 +00:00
|
|
|
|
const usageRaw = parsed.message?.usage ?? parsed.usage;
|
|
|
|
|
|
const usage = normalizeUsage(usageRaw);
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (usage) {
|
|
|
|
|
|
lastUsage = usage;
|
|
|
|
|
|
}
|
2025-12-08 14:46:15 +00:00
|
|
|
|
model = parsed.message?.model ?? parsed.model ?? model;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore bad lines
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (!lastUsage) {
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
2025-12-12 23:22:05 +00:00
|
|
|
|
input = lastUsage.input ?? 0;
|
|
|
|
|
|
output = lastUsage.output ?? 0;
|
2026-01-14 14:31:43 +00:00
|
|
|
|
promptTokens = derivePromptTokens(lastUsage) ?? lastUsage.total ?? input + output;
|
2025-12-12 23:22:05 +00:00
|
|
|
|
const total = lastUsage.total ?? promptTokens + output;
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (promptTokens === 0 && total === 0) {
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
2025-12-12 23:22:05 +00:00
|
|
|
|
return { input, output, promptTokens, total, model };
|
2025-12-08 14:46:15 +00:00
|
|
|
|
} catch {
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-09 02:21:17 +00:00
|
|
|
|
const formatUsagePair = (input?: number | null, output?: number | null) => {
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (input == null && output == null) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-01-09 02:21:17 +00:00
|
|
|
|
const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?";
|
2026-01-14 14:31:43 +00:00
|
|
|
|
const outputLabel = typeof output === "number" ? formatTokenCount(output) : "?";
|
2026-01-09 03:14:39 +00:00
|
|
|
|
return `🧮 Tokens: ${inputLabel} in / ${outputLabel} out`;
|
2026-01-09 02:21:17 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-17 11:22:31 +09:00
|
|
|
|
const formatMediaUnderstandingLine = (decisions?: ReadonlyArray<MediaUnderstandingDecision>) => {
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (!decisions || decisions.length === 0) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-01-17 07:17:09 +00:00
|
|
|
|
const parts = decisions
|
|
|
|
|
|
.map((decision) => {
|
|
|
|
|
|
const count = decision.attachments.length;
|
|
|
|
|
|
const countLabel = count > 1 ? ` x${count}` : "";
|
|
|
|
|
|
if (decision.outcome === "success") {
|
|
|
|
|
|
const chosen = decision.attachments.find((entry) => entry.chosen)?.chosen;
|
|
|
|
|
|
const provider = chosen?.provider?.trim();
|
|
|
|
|
|
const model = chosen?.model?.trim();
|
|
|
|
|
|
const modelLabel = provider ? (model ? `${provider}/${model}` : provider) : null;
|
|
|
|
|
|
return `${decision.capability}${countLabel} ok${modelLabel ? ` (${modelLabel})` : ""}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (decision.outcome === "no-attachment") {
|
|
|
|
|
|
return `${decision.capability} none`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (decision.outcome === "disabled") {
|
|
|
|
|
|
return `${decision.capability} off`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (decision.outcome === "scope-deny") {
|
|
|
|
|
|
return `${decision.capability} denied`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (decision.outcome === "skipped") {
|
|
|
|
|
|
const reason = decision.attachments
|
|
|
|
|
|
.flatMap((entry) => entry.attempts.map((attempt) => attempt.reason).filter(Boolean))
|
|
|
|
|
|
.find(Boolean);
|
|
|
|
|
|
const shortReason = reason ? reason.split(":")[0]?.trim() : undefined;
|
|
|
|
|
|
return `${decision.capability} skipped${shortReason ? ` (${shortReason})` : ""}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
})
|
2026-01-21 03:43:42 +00:00
|
|
|
|
.filter((part): part is string => part != null);
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (parts.length === 0) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (parts.every((part) => part.endsWith(" none"))) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-01-17 07:17:09 +00:00
|
|
|
|
return `📎 Media: ${parts.join(" · ")}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-24 23:35:20 -05:00
|
|
|
|
const formatVoiceModeLine = (
|
2026-01-30 03:15:10 +01:00
|
|
|
|
config?: OpenClawConfig,
|
2026-01-24 23:35:20 -05:00
|
|
|
|
sessionEntry?: SessionEntry,
|
|
|
|
|
|
): string | null => {
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (!config) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-01-24 10:03:19 +00:00
|
|
|
|
const ttsConfig = resolveTtsConfig(config);
|
|
|
|
|
|
const prefsPath = resolveTtsPrefsPath(ttsConfig);
|
2026-01-24 23:35:20 -05:00
|
|
|
|
const autoMode = resolveTtsAutoMode({
|
|
|
|
|
|
config: ttsConfig,
|
|
|
|
|
|
prefsPath,
|
|
|
|
|
|
sessionAuto: sessionEntry?.ttsAuto,
|
|
|
|
|
|
});
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (autoMode === "off") {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-01-24 10:03:19 +00:00
|
|
|
|
const provider = getTtsProvider(ttsConfig, prefsPath);
|
|
|
|
|
|
const maxLength = getTtsMaxLength(prefsPath);
|
|
|
|
|
|
const summarize = isSummarizationEnabled(prefsPath) ? "on" : "off";
|
2026-01-24 23:35:20 -05:00
|
|
|
|
return `🔊 Voice: ${autoMode} · provider=${provider} · limit=${maxLength} · summary=${summarize}`;
|
2026-01-24 10:03:19 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-07 16:53:19 +00:00
|
|
|
|
export function buildStatusMessage(args: StatusArgs): string {
|
2026-01-09 03:14:39 +00:00
|
|
|
|
const now = args.now ?? Date.now();
|
2025-12-07 16:53:19 +00:00
|
|
|
|
const entry = args.sessionEntry;
|
2025-12-26 00:16:29 +01:00
|
|
|
|
const resolved = resolveConfiguredModelRef({
|
2026-01-09 12:44:23 +00:00
|
|
|
|
cfg: {
|
|
|
|
|
|
agents: {
|
|
|
|
|
|
defaults: args.agent ?? {},
|
|
|
|
|
|
},
|
2026-01-30 03:15:10 +01:00
|
|
|
|
} as OpenClawConfig,
|
2025-12-26 00:16:29 +01:00
|
|
|
|
defaultProvider: DEFAULT_PROVIDER,
|
|
|
|
|
|
defaultModel: DEFAULT_MODEL,
|
|
|
|
|
|
});
|
2026-02-19 14:33:02 -08:00
|
|
|
|
const selectedProvider = entry?.providerOverride ?? resolved.provider ?? DEFAULT_PROVIDER;
|
|
|
|
|
|
const selectedModel = entry?.modelOverride ?? resolved.model ?? DEFAULT_MODEL;
|
|
|
|
|
|
const modelRefs = resolveSelectedAndActiveModel({
|
|
|
|
|
|
selectedProvider,
|
|
|
|
|
|
selectedModel,
|
|
|
|
|
|
sessionEntry: entry,
|
|
|
|
|
|
});
|
|
|
|
|
|
let activeProvider = modelRefs.active.provider;
|
|
|
|
|
|
let activeModel = modelRefs.active.model;
|
2025-12-08 14:46:15 +00:00
|
|
|
|
let contextTokens =
|
2025-12-07 16:53:19 +00:00
|
|
|
|
entry?.contextTokens ??
|
2025-12-17 11:29:04 +01:00
|
|
|
|
args.agent?.contextTokens ??
|
2026-02-19 14:33:02 -08:00
|
|
|
|
lookupContextTokens(activeModel) ??
|
2025-12-07 16:53:19 +00:00
|
|
|
|
DEFAULT_CONTEXT_TOKENS;
|
2025-12-08 14:46:15 +00:00
|
|
|
|
|
2026-01-09 02:21:17 +00:00
|
|
|
|
let inputTokens = entry?.inputTokens;
|
|
|
|
|
|
let outputTokens = entry?.outputTokens;
|
2026-02-13 14:17:24 +01:00
|
|
|
|
let totalTokens = entry?.totalTokens ?? (entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0);
|
2025-12-08 14:46:15 +00:00
|
|
|
|
|
2025-12-12 23:22:05 +00:00
|
|
|
|
// Prefer prompt-size tokens from the session transcript when it looks larger
|
|
|
|
|
|
// (cached prompt tokens are often missing from agent meta/store).
|
2026-01-07 07:21:56 +01:00
|
|
|
|
if (args.includeTranscriptUsage) {
|
2026-02-12 23:23:12 -05:00
|
|
|
|
const logUsage = readUsageFromSessionLog(
|
|
|
|
|
|
entry?.sessionId,
|
|
|
|
|
|
entry,
|
2026-02-13 14:17:24 +01:00
|
|
|
|
args.agentId,
|
2026-02-12 23:23:12 -05:00
|
|
|
|
args.sessionKey,
|
|
|
|
|
|
args.sessionStorePath,
|
|
|
|
|
|
);
|
2026-01-07 07:21:56 +01:00
|
|
|
|
if (logUsage) {
|
|
|
|
|
|
const candidate = logUsage.promptTokens || logUsage.total;
|
|
|
|
|
|
if (!totalTokens || totalTokens === 0 || candidate > totalTokens) {
|
|
|
|
|
|
totalTokens = candidate;
|
|
|
|
|
|
}
|
2026-02-19 14:33:02 -08:00
|
|
|
|
if (!entry?.model && logUsage.model) {
|
|
|
|
|
|
const slashIndex = logUsage.model.indexOf("/");
|
|
|
|
|
|
if (slashIndex > 0) {
|
|
|
|
|
|
const provider = logUsage.model.slice(0, slashIndex).trim();
|
|
|
|
|
|
const model = logUsage.model.slice(slashIndex + 1).trim();
|
|
|
|
|
|
if (provider && model) {
|
|
|
|
|
|
activeProvider = provider;
|
|
|
|
|
|
activeModel = model;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
activeModel = logUsage.model;
|
|
|
|
|
|
}
|
2026-01-31 16:19:20 +09:00
|
|
|
|
}
|
2026-01-07 07:21:56 +01:00
|
|
|
|
if (!contextTokens && logUsage.model) {
|
|
|
|
|
|
contextTokens = lookupContextTokens(logUsage.model) ?? contextTokens;
|
|
|
|
|
|
}
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (!inputTokens || inputTokens === 0) {
|
|
|
|
|
|
inputTokens = logUsage.input;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!outputTokens || outputTokens === 0) {
|
|
|
|
|
|
outputTokens = logUsage.output;
|
|
|
|
|
|
}
|
2025-12-08 14:46:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-07 16:53:19 +00:00
|
|
|
|
|
2025-12-17 11:29:04 +01:00
|
|
|
|
const thinkLevel = args.resolvedThink ?? args.agent?.thinkingDefault ?? "off";
|
2026-01-14 14:31:43 +00:00
|
|
|
|
const verboseLevel = args.resolvedVerbose ?? args.agent?.verboseDefault ?? "off";
|
2026-01-07 06:16:38 +01:00
|
|
|
|
const reasoningLevel = args.resolvedReasoning ?? "off";
|
2026-01-04 05:15:42 +00:00
|
|
|
|
const elevatedLevel =
|
2026-01-05 07:07:17 +01:00
|
|
|
|
args.resolvedElevated ??
|
2026-01-05 07:12:13 +01:00
|
|
|
|
args.sessionEntry?.elevatedLevel ??
|
|
|
|
|
|
args.agent?.elevatedDefault ??
|
|
|
|
|
|
"on";
|
2025-12-07 16:53:19 +00:00
|
|
|
|
|
2026-01-21 09:07:21 +00:00
|
|
|
|
const runtime = { label: resolveRuntimeLabel(args) };
|
2026-01-09 03:14:39 +00:00
|
|
|
|
|
|
|
|
|
|
const updatedAt = entry?.updatedAt;
|
|
|
|
|
|
const sessionLine = [
|
|
|
|
|
|
`Session: ${args.sessionKey ?? "unknown"}`,
|
2026-02-08 04:53:31 -08:00
|
|
|
|
typeof updatedAt === "number" ? `updated ${formatTimeAgo(now - updatedAt)}` : "no activity",
|
2026-01-09 03:14:39 +00:00
|
|
|
|
]
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join(" • ");
|
|
|
|
|
|
|
2026-01-02 10:14:58 +01:00
|
|
|
|
const isGroupSession =
|
|
|
|
|
|
entry?.chatType === "group" ||
|
2026-01-17 04:04:05 +00:00
|
|
|
|
entry?.chatType === "channel" ||
|
2026-01-02 10:14:58 +01:00
|
|
|
|
Boolean(args.sessionKey?.includes(":group:")) ||
|
2026-01-17 08:46:19 +00:00
|
|
|
|
Boolean(args.sessionKey?.includes(":channel:"));
|
2026-01-07 07:21:56 +01:00
|
|
|
|
const groupActivationValue = isGroupSession
|
|
|
|
|
|
? (args.groupActivation ?? entry?.groupActivation ?? "mention")
|
2025-12-22 20:45:22 +00:00
|
|
|
|
: undefined;
|
2025-12-22 20:36:29 +01:00
|
|
|
|
|
2026-01-09 03:14:39 +00:00
|
|
|
|
const contextLine = [
|
|
|
|
|
|
`Context: ${formatTokens(totalTokens, contextTokens ?? null)}`,
|
|
|
|
|
|
`🧹 Compactions: ${entry?.compactionCount ?? 0}`,
|
|
|
|
|
|
]
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join(" · ");
|
|
|
|
|
|
|
|
|
|
|
|
const queueMode = args.queue?.mode ?? "unknown";
|
|
|
|
|
|
const queueDetails = formatQueueDetails(args.queue);
|
2026-01-17 05:33:27 +00:00
|
|
|
|
const verboseLabel =
|
|
|
|
|
|
verboseLevel === "full" ? "verbose:full" : verboseLevel === "on" ? "verbose" : null;
|
2026-01-22 05:32:13 +00:00
|
|
|
|
const elevatedLabel =
|
|
|
|
|
|
elevatedLevel && elevatedLevel !== "off"
|
|
|
|
|
|
? elevatedLevel === "on"
|
|
|
|
|
|
? "elevated"
|
|
|
|
|
|
: `elevated:${elevatedLevel}`
|
|
|
|
|
|
: null;
|
2026-01-09 03:14:39 +00:00
|
|
|
|
const optionParts = [
|
|
|
|
|
|
`Runtime: ${runtime.label}`,
|
|
|
|
|
|
`Think: ${thinkLevel}`,
|
2026-01-09 23:41:52 +00:00
|
|
|
|
verboseLabel,
|
2026-01-09 03:14:39 +00:00
|
|
|
|
reasoningLevel !== "off" ? `Reasoning: ${reasoningLevel}` : null,
|
2026-01-09 23:41:52 +00:00
|
|
|
|
elevatedLabel,
|
2026-01-09 03:14:39 +00:00
|
|
|
|
];
|
|
|
|
|
|
const optionsLine = optionParts.filter(Boolean).join(" · ");
|
|
|
|
|
|
const activationParts = [
|
|
|
|
|
|
groupActivationValue ? `👥 Activation: ${groupActivationValue}` : null,
|
|
|
|
|
|
`🪢 Queue: ${queueMode}${queueDetails}`,
|
|
|
|
|
|
];
|
|
|
|
|
|
const activationLine = activationParts.filter(Boolean).join(" · ");
|
|
|
|
|
|
|
2026-02-19 14:33:02 -08:00
|
|
|
|
const activeAuthMode = resolveModelAuthMode(activeProvider, args.config);
|
|
|
|
|
|
const selectedAuthLabelValue =
|
|
|
|
|
|
args.modelAuth ??
|
|
|
|
|
|
(() => {
|
|
|
|
|
|
const selectedAuthMode = resolveModelAuthMode(selectedProvider, args.config);
|
|
|
|
|
|
return selectedAuthMode && selectedAuthMode !== "unknown" ? selectedAuthMode : undefined;
|
|
|
|
|
|
})();
|
|
|
|
|
|
const activeAuthLabelValue =
|
|
|
|
|
|
args.activeModelAuth ??
|
|
|
|
|
|
(activeAuthMode && activeAuthMode !== "unknown" ? activeAuthMode : undefined);
|
|
|
|
|
|
const showCost = activeAuthLabelValue === "api-key" || activeAuthLabelValue === "mixed";
|
2026-01-09 02:21:17 +00:00
|
|
|
|
const costConfig = showCost
|
|
|
|
|
|
? resolveModelCostConfig({
|
2026-02-19 14:33:02 -08:00
|
|
|
|
provider: activeProvider,
|
|
|
|
|
|
model: activeModel,
|
2026-01-09 02:21:17 +00:00
|
|
|
|
config: args.config,
|
|
|
|
|
|
})
|
|
|
|
|
|
: undefined;
|
2026-01-14 14:31:43 +00:00
|
|
|
|
const hasUsage = typeof inputTokens === "number" || typeof outputTokens === "number";
|
2026-01-09 02:21:17 +00:00
|
|
|
|
const cost =
|
|
|
|
|
|
showCost && hasUsage
|
|
|
|
|
|
? estimateUsageCost({
|
|
|
|
|
|
usage: {
|
|
|
|
|
|
input: inputTokens ?? undefined,
|
|
|
|
|
|
output: outputTokens ?? undefined,
|
|
|
|
|
|
},
|
|
|
|
|
|
cost: costConfig,
|
|
|
|
|
|
})
|
|
|
|
|
|
: undefined;
|
|
|
|
|
|
const costLabel = showCost && hasUsage ? formatUsd(cost) : undefined;
|
|
|
|
|
|
|
2026-02-19 14:33:02 -08:00
|
|
|
|
const selectedModelLabel = modelRefs.selected.label || "unknown";
|
|
|
|
|
|
const activeModelLabel = formatProviderModelRef(activeProvider, activeModel) || "unknown";
|
|
|
|
|
|
const fallbackState = resolveActiveFallbackState({
|
|
|
|
|
|
selectedModelRef: selectedModelLabel,
|
|
|
|
|
|
activeModelRef: activeModelLabel,
|
|
|
|
|
|
state: entry,
|
|
|
|
|
|
});
|
|
|
|
|
|
const selectedAuthLabel = selectedAuthLabelValue ? ` · 🔑 ${selectedAuthLabelValue}` : "";
|
|
|
|
|
|
const modelLine = `🧠 Model: ${selectedModelLabel}${selectedAuthLabel}`;
|
|
|
|
|
|
const showFallbackAuth = activeAuthLabelValue && activeAuthLabelValue !== selectedAuthLabelValue;
|
|
|
|
|
|
const fallbackLine = fallbackState.active
|
|
|
|
|
|
? `↪️ Fallback: ${activeModelLabel}${
|
|
|
|
|
|
showFallbackAuth ? ` · 🔑 ${activeAuthLabelValue}` : ""
|
|
|
|
|
|
} (${fallbackState.reason ?? "selected model unavailable"})`
|
|
|
|
|
|
: null;
|
2026-01-09 03:14:39 +00:00
|
|
|
|
const commit = resolveCommitHash();
|
2026-01-30 03:15:10 +01:00
|
|
|
|
const versionLine = `🦞 OpenClaw ${VERSION}${commit ? ` (${commit})` : ""}`;
|
2026-01-09 02:21:17 +00:00
|
|
|
|
const usagePair = formatUsagePair(inputTokens, outputTokens);
|
2026-01-09 03:14:39 +00:00
|
|
|
|
const costLine = costLabel ? `💵 Cost: ${costLabel}` : null;
|
2026-01-09 03:45:01 +00:00
|
|
|
|
const usageCostLine =
|
2026-01-14 14:31:43 +00:00
|
|
|
|
usagePair && costLine ? `${usagePair} · ${costLine}` : (usagePair ?? costLine);
|
2026-01-17 07:17:09 +00:00
|
|
|
|
const mediaLine = formatMediaUnderstandingLine(args.mediaDecisions);
|
2026-01-24 23:35:20 -05:00
|
|
|
|
const voiceLine = formatVoiceModeLine(args.config, args.sessionEntry);
|
2025-12-07 16:53:19 +00:00
|
|
|
|
|
2026-01-09 03:14:39 +00:00
|
|
|
|
return [
|
|
|
|
|
|
versionLine,
|
2026-01-24 06:22:54 +00:00
|
|
|
|
args.timeLine,
|
2026-01-09 03:14:39 +00:00
|
|
|
|
modelLine,
|
2026-02-19 14:33:02 -08:00
|
|
|
|
fallbackLine,
|
2026-01-09 03:45:01 +00:00
|
|
|
|
usageCostLine,
|
2026-01-09 03:14:39 +00:00
|
|
|
|
`📚 ${contextLine}`,
|
2026-01-17 07:17:09 +00:00
|
|
|
|
mediaLine,
|
2026-01-09 03:14:39 +00:00
|
|
|
|
args.usageLine,
|
|
|
|
|
|
`🧵 ${sessionLine}`,
|
2026-01-18 04:44:52 +00:00
|
|
|
|
args.subagentsLine,
|
2026-01-09 03:14:39 +00:00
|
|
|
|
`⚙️ ${optionsLine}`,
|
2026-01-24 10:03:19 +00:00
|
|
|
|
voiceLine,
|
2026-01-09 03:14:39 +00:00
|
|
|
|
activationLine,
|
|
|
|
|
|
]
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join("\n");
|
2026-01-05 07:07:17 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 09:37:22 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
|
export function buildHelpMessage(cfg?: OpenClawConfig): string {
|
2026-01-27 09:37:22 +08:00
|
|
|
|
const lines = ["ℹ️ Help", ""];
|
|
|
|
|
|
|
|
|
|
|
|
lines.push("Session");
|
|
|
|
|
|
lines.push(" /new | /reset | /compact [instructions] | /stop");
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
|
|
const optionParts = ["/think <level>", "/model <id>", "/verbose on|off"];
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (cfg?.commands?.config === true) {
|
|
|
|
|
|
optionParts.push("/config");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (cfg?.commands?.debug === true) {
|
|
|
|
|
|
optionParts.push("/debug");
|
|
|
|
|
|
}
|
2026-01-27 09:37:22 +08:00
|
|
|
|
lines.push("Options");
|
|
|
|
|
|
lines.push(` ${optionParts.join(" | ")}`);
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
|
|
lines.push("Status");
|
|
|
|
|
|
lines.push(" /status | /whoami | /context");
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (seen.has(key)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-01-27 09:37:22 +08:00
|
|
|
|
seen.add(key);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
const aliasLabel = aliases.length ? ` (${aliases.join(", ")})` : "";
|
|
|
|
|
|
const scopeLabel = command.scope === "text" ? " [text]" : "";
|
|
|
|
|
|
return `${primary}${aliasLabel}${scopeLabel} - ${command.description}`;
|
2025-12-07 16:53:19 +00:00
|
|
|
|
}
|
2026-01-08 16:02:54 +01:00
|
|
|
|
|
2026-01-27 02:35:09 -05:00
|
|
|
|
type CommandsListItem = {
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
text: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function buildCommandItems(
|
|
|
|
|
|
commands: ChatCommandDefinition[],
|
|
|
|
|
|
pluginCommands: ReturnType<typeof listPluginCommands>,
|
|
|
|
|
|
): CommandsListItem[] {
|
|
|
|
|
|
const grouped = groupCommandsByCategory(commands);
|
|
|
|
|
|
const items: CommandsListItem[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const category of CATEGORY_ORDER) {
|
|
|
|
|
|
const categoryCommands = grouped.get(category) ?? [];
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (categoryCommands.length === 0) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-01-27 02:35:09 -05:00
|
|
|
|
const label = CATEGORY_LABELS[category];
|
|
|
|
|
|
for (const command of categoryCommands) {
|
|
|
|
|
|
items.push({ label, text: formatCommandEntry(command) });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const command of pluginCommands) {
|
|
|
|
|
|
const pluginLabel = command.pluginId ? ` (${command.pluginId})` : "";
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
label: "Plugins",
|
|
|
|
|
|
text: `/${command.name}${pluginLabel} - ${command.description}`,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return items;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatCommandList(items: CommandsListItem[]): string {
|
|
|
|
|
|
const lines: string[] = [];
|
|
|
|
|
|
let currentLabel: string | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
|
if (item.label !== currentLabel) {
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (lines.length > 0) {
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
}
|
2026-01-27 02:35:09 -05:00
|
|
|
|
lines.push(item.label);
|
|
|
|
|
|
currentLabel = item.label;
|
|
|
|
|
|
}
|
|
|
|
|
|
lines.push(` ${item.text}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return lines.join("\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 12:10:20 +00:00
|
|
|
|
export function buildCommandsMessage(
|
2026-01-30 03:15:10 +01:00
|
|
|
|
cfg?: OpenClawConfig,
|
2026-01-16 12:10:20 +00:00
|
|
|
|
skillCommands?: SkillCommandSpec[],
|
2026-01-27 09:37:22 +08:00
|
|
|
|
options?: CommandsMessageOptions,
|
2026-01-16 12:10:20 +00:00
|
|
|
|
): string {
|
2026-01-27 09:37:22 +08:00
|
|
|
|
const result = buildCommandsMessagePaginated(cfg, skillCommands, options);
|
|
|
|
|
|
return result.text;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildCommandsMessagePaginated(
|
2026-01-30 03:15:10 +01:00
|
|
|
|
cfg?: OpenClawConfig,
|
2026-01-27 09:37:22 +08:00
|
|
|
|
skillCommands?: SkillCommandSpec[],
|
|
|
|
|
|
options?: CommandsMessageOptions,
|
|
|
|
|
|
): CommandsMessageResult {
|
|
|
|
|
|
const page = Math.max(1, options?.page ?? 1);
|
|
|
|
|
|
const surface = options?.surface?.toLowerCase();
|
|
|
|
|
|
const isTelegram = surface === "telegram";
|
|
|
|
|
|
|
2026-01-16 12:10:20 +00:00
|
|
|
|
const commands = cfg
|
|
|
|
|
|
? listChatCommandsForConfig(cfg, { skillCommands })
|
|
|
|
|
|
: listChatCommands({ skillCommands });
|
2026-01-25 07:22:36 -05:00
|
|
|
|
const pluginCommands = listPluginCommands();
|
2026-01-27 02:35:09 -05:00
|
|
|
|
const items = buildCommandItems(commands, pluginCommands);
|
2026-01-27 09:37:22 +08:00
|
|
|
|
|
|
|
|
|
|
if (!isTelegram) {
|
|
|
|
|
|
const lines = ["ℹ️ Slash commands", ""];
|
2026-01-27 02:35:09 -05:00
|
|
|
|
lines.push(formatCommandList(items));
|
2026-01-27 09:37:22 +08:00
|
|
|
|
return {
|
|
|
|
|
|
text: lines.join("\n").trim(),
|
|
|
|
|
|
totalPages: 1,
|
|
|
|
|
|
currentPage: 1,
|
|
|
|
|
|
hasNext: false,
|
|
|
|
|
|
hasPrev: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 02:35:09 -05:00
|
|
|
|
const totalCommands = items.length;
|
2026-01-27 09:37:22 +08:00
|
|
|
|
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;
|
2026-01-27 02:35:09 -05:00
|
|
|
|
const pageItems = items.slice(startIndex, endIndex);
|
2026-01-27 09:37:22 +08:00
|
|
|
|
|
|
|
|
|
|
const lines = [`ℹ️ Commands (${currentPage}/${totalPages})`, ""];
|
2026-01-27 02:35:09 -05:00
|
|
|
|
lines.push(formatCommandList(pageItems));
|
2026-01-27 09:37:22 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
2026-01-27 02:35:09 -05:00
|
|
|
|
text: lines.join("\n").trim(),
|
2026-01-27 09:37:22 +08:00
|
|
|
|
totalPages,
|
|
|
|
|
|
currentPage,
|
|
|
|
|
|
hasNext: currentPage < totalPages,
|
|
|
|
|
|
hasPrev: currentPage > 1,
|
|
|
|
|
|
};
|
2026-01-08 16:02:54 +01:00
|
|
|
|
}
|