fix: restore emoji-rich status output
This commit is contained in:
@@ -11,7 +11,7 @@ read_when:
|
||||
- No estimated costs; only the provider-reported windows.
|
||||
|
||||
## Where it shows up
|
||||
- `/status` in chats: compact one‑liner with session tokens + estimated cost (API key only) and provider quota windows when available.
|
||||
- `/status` in chats: emoji‑rich status card with session tokens + estimated cost (API key only) and provider quota windows when available.
|
||||
- `/cost on|off` in chats: toggles per‑response usage lines (OAuth shows tokens only).
|
||||
- CLI: `clawdbot status --usage` prints a full per-provider breakdown.
|
||||
- CLI: `clawdbot providers list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
|
||||
|
||||
@@ -38,7 +38,7 @@ Everything the model receives counts toward the context limit:
|
||||
|
||||
Use these in chat:
|
||||
|
||||
- `/status` → **compact one‑liner** with the session model, context usage,
|
||||
- `/status` → **emoji‑rich status card** with the session model, context usage,
|
||||
last response input/output tokens, and **estimated cost** (API key only).
|
||||
- `/cost on|off` → appends a **per-response usage line** to every reply.
|
||||
- Persists per session (stored as `responseUsage`).
|
||||
|
||||
@@ -213,7 +213,7 @@ describe("trigger handling", () => {
|
||||
makeCfg(home),
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("status");
|
||||
expect(text).toContain("ClawdBot");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -164,6 +164,7 @@ export async function buildStatusReply(params: {
|
||||
defaultGroupActivation())
|
||||
: undefined;
|
||||
const statusText = buildStatusMessage({
|
||||
config: cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model: {
|
||||
@@ -554,6 +555,9 @@ export async function handleCommands(params: {
|
||||
const reply = await buildStatusReply({
|
||||
cfg,
|
||||
command,
|
||||
provider: command.provider,
|
||||
sessionEntry,
|
||||
});
|
||||
sessionEntry,
|
||||
sessionKey,
|
||||
sessionScope,
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { buildStatusMessage } from "./status.js";
|
||||
|
||||
const HOME_ENV_KEYS = ["HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH"] as const;
|
||||
@@ -45,6 +46,27 @@ afterEach(() => {
|
||||
describe("buildStatusMessage", () => {
|
||||
it("summarizes agent readiness and context usage", () => {
|
||||
const text = buildStatusMessage({
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
anthropic: {
|
||||
apiKey: "test-key",
|
||||
models: [
|
||||
{
|
||||
id: "pi:opus",
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 1,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig,
|
||||
agent: {
|
||||
model: "anthropic/pi:opus",
|
||||
contextTokens: 32_000,
|
||||
@@ -52,6 +74,8 @@ describe("buildStatusMessage", () => {
|
||||
sessionEntry: {
|
||||
sessionId: "abc",
|
||||
updatedAt: 0,
|
||||
inputTokens: 1200,
|
||||
outputTokens: 800,
|
||||
totalTokens: 16_000,
|
||||
contextTokens: 32_000,
|
||||
thinkingLevel: "low",
|
||||
@@ -64,17 +88,22 @@ describe("buildStatusMessage", () => {
|
||||
resolvedVerbose: "off",
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
modelAuth: "api-key",
|
||||
now: 10 * 60_000, // 10 minutes later
|
||||
});
|
||||
|
||||
expect(text).toContain("status agent:main:main");
|
||||
expect(text).toContain("model anthropic/pi:opus (api-key)");
|
||||
expect(text).toContain("Context 16k/32k (50%)");
|
||||
expect(text).toContain("compactions 2");
|
||||
expect(text).toContain("think medium");
|
||||
expect(text).toContain("verbose off");
|
||||
expect(text).toContain("reasoning off");
|
||||
expect(text).toContain("elevated on");
|
||||
expect(text).toContain("queue collect");
|
||||
expect(text).toContain("🦞 ClawdBot");
|
||||
expect(text).toContain("🧠 Model: anthropic/pi:opus · 🔑 api-key");
|
||||
expect(text).toContain("🧮 Tokens: 1.2k in / 800 out");
|
||||
expect(text).toContain("💵 Cost: $0.0020");
|
||||
expect(text).toContain("Context: 16k/32k (50%)");
|
||||
expect(text).toContain("🧹 Compactions: 2");
|
||||
expect(text).toContain("Session: agent:main:main");
|
||||
expect(text).toContain("updated 10m ago");
|
||||
expect(text).toContain("Runtime: direct");
|
||||
expect(text).toContain("Think: medium");
|
||||
expect(text).toContain("Verbose: off");
|
||||
expect(text).toContain("Elevated: on");
|
||||
expect(text).toContain("Queue: collect");
|
||||
});
|
||||
|
||||
it("shows verbose/elevated labels only when enabled", () => {
|
||||
@@ -89,8 +118,8 @@ describe("buildStatusMessage", () => {
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
});
|
||||
|
||||
expect(text).toContain("verbose on");
|
||||
expect(text).toContain("elevated on");
|
||||
expect(text).toContain("Verbose: on");
|
||||
expect(text).toContain("Elevated: on");
|
||||
});
|
||||
|
||||
it("prefers model overrides over last-run model", () => {
|
||||
@@ -114,7 +143,7 @@ describe("buildStatusMessage", () => {
|
||||
modelAuth: "api-key",
|
||||
});
|
||||
|
||||
expect(text).toContain("model openai/gpt-4.1-mini");
|
||||
expect(text).toContain("🧠 Model: openai/gpt-4.1-mini");
|
||||
});
|
||||
|
||||
it("keeps provider prefix from configured model", () => {
|
||||
@@ -127,7 +156,7 @@ describe("buildStatusMessage", () => {
|
||||
modelAuth: "api-key",
|
||||
});
|
||||
|
||||
expect(text).toContain("model google-antigravity/claude-sonnet-4-5");
|
||||
expect(text).toContain("🧠 Model: google-antigravity/claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
it("handles missing agent config gracefully", () => {
|
||||
@@ -138,9 +167,9 @@ describe("buildStatusMessage", () => {
|
||||
modelAuth: "api-key",
|
||||
});
|
||||
|
||||
expect(text).toContain("model");
|
||||
expect(text).toContain("Context");
|
||||
expect(text).toContain("queue collect");
|
||||
expect(text).toContain("🧠 Model:");
|
||||
expect(text).toContain("Context:");
|
||||
expect(text).toContain("Queue: collect");
|
||||
});
|
||||
|
||||
it("includes group activation for group sessions", () => {
|
||||
@@ -158,7 +187,7 @@ describe("buildStatusMessage", () => {
|
||||
modelAuth: "api-key",
|
||||
});
|
||||
|
||||
expect(text).toContain("activation always");
|
||||
expect(text).toContain("Activation: always");
|
||||
});
|
||||
|
||||
it("shows queue details when overridden", () => {
|
||||
@@ -179,7 +208,7 @@ describe("buildStatusMessage", () => {
|
||||
});
|
||||
|
||||
expect(text).toContain(
|
||||
"queue collect (depth 3 · debounce 2s · cap 5 · drop old)",
|
||||
"Queue: collect (depth 3 · debounce 2s · cap 5 · drop old)",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -194,7 +223,43 @@ describe("buildStatusMessage", () => {
|
||||
modelAuth: "api-key",
|
||||
});
|
||||
|
||||
expect(text).toContain("📊 Usage: Claude 80% left (5h)");
|
||||
const lines = text.split("\n");
|
||||
const contextIndex = lines.findIndex((line) => line.startsWith("📚 "));
|
||||
expect(contextIndex).toBeGreaterThan(-1);
|
||||
expect(lines[contextIndex + 1]).toBe("📊 Usage: Claude 80% left (5h)");
|
||||
});
|
||||
|
||||
it("hides cost when not using an API key", () => {
|
||||
const text = buildStatusMessage({
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
anthropic: {
|
||||
models: [
|
||||
{
|
||||
id: "claude-opus-4-5",
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 1,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig,
|
||||
agent: { model: "anthropic/claude-opus-4-5" },
|
||||
sessionEntry: { sessionId: "c1", updatedAt: 0, inputTokens: 10 },
|
||||
sessionKey: "agent:main:main",
|
||||
sessionScope: "per-sender",
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
modelAuth: "oauth",
|
||||
});
|
||||
|
||||
expect(text).not.toContain("💵 Cost:");
|
||||
});
|
||||
|
||||
it("prefers cached prompt tokens from the session log", async () => {
|
||||
@@ -257,7 +322,7 @@ describe("buildStatusMessage", () => {
|
||||
modelAuth: "api-key",
|
||||
});
|
||||
|
||||
expect(text).toContain("Context 1.0k/32k");
|
||||
expect(text).toContain("Context: 1.0k/32k");
|
||||
} finally {
|
||||
restoreHomeEnv(previousHome);
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
|
||||
@@ -15,16 +15,19 @@ import {
|
||||
} from "../agents/usage.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
resolveMainSessionKey,
|
||||
resolveSessionFilePath,
|
||||
type SessionEntry,
|
||||
type SessionScope,
|
||||
} from "../config/sessions.js";
|
||||
import { resolveCommitHash } from "../infra/git-commit.js";
|
||||
import {
|
||||
estimateUsageCost,
|
||||
formatTokenCount as formatTokenCountShared,
|
||||
formatUsd,
|
||||
resolveModelCostConfig,
|
||||
} from "../utils/usage-format.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import type {
|
||||
ElevatedLevel,
|
||||
ReasoningLevel,
|
||||
@@ -60,6 +63,7 @@ type StatusArgs = {
|
||||
usageLine?: string;
|
||||
queue?: QueueStatus;
|
||||
includeTranscriptUsage?: boolean;
|
||||
now?: number;
|
||||
};
|
||||
|
||||
const formatTokens = (
|
||||
@@ -82,6 +86,17 @@ export const formatContextUsageShort = (
|
||||
contextTokens: number | null | undefined,
|
||||
) => `Context ${formatTokens(total, contextTokens ?? null)}`;
|
||||
|
||||
const formatAge = (ms?: number | null) => {
|
||||
if (!ms || ms < 0) return "unknown";
|
||||
const minutes = Math.round(ms / 60_000);
|
||||
if (minutes < 1) return "just now";
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 48) return `${hours}h ago`;
|
||||
const days = Math.round(hours / 24);
|
||||
return `${days}d ago`;
|
||||
};
|
||||
|
||||
const formatQueueDetails = (queue?: QueueStatus) => {
|
||||
if (!queue) return "";
|
||||
const depth = typeof queue.depth === "number" ? `depth ${queue.depth}` : null;
|
||||
@@ -166,10 +181,11 @@ const formatUsagePair = (input?: number | null, output?: number | null) => {
|
||||
const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?";
|
||||
const outputLabel =
|
||||
typeof output === "number" ? formatTokenCount(output) : "?";
|
||||
return `usage ${inputLabel} in / ${outputLabel} out`;
|
||||
return `🧮 Tokens: ${inputLabel} in / ${outputLabel} out`;
|
||||
};
|
||||
|
||||
export function buildStatusMessage(args: StatusArgs): string {
|
||||
const now = args.now ?? Date.now();
|
||||
const entry = args.sessionEntry;
|
||||
const resolved = resolveConfiguredModelRef({
|
||||
cfg: { agent: args.agent ?? {} },
|
||||
@@ -219,6 +235,33 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
args.agent?.elevatedDefault ??
|
||||
"on";
|
||||
|
||||
const runtime = (() => {
|
||||
const sandboxMode = args.agent?.sandbox?.mode ?? "off";
|
||||
if (sandboxMode === "off") return { label: "direct" };
|
||||
const sessionScope = args.sessionScope ?? "per-sender";
|
||||
const mainKey = resolveMainSessionKey({
|
||||
session: { scope: sessionScope },
|
||||
});
|
||||
const sessionKey = args.sessionKey?.trim();
|
||||
const sandboxed = sessionKey
|
||||
? sandboxMode === "all" || sessionKey !== mainKey.trim()
|
||||
: false;
|
||||
const runtime = sandboxed ? "docker" : sessionKey ? "direct" : "unknown";
|
||||
return {
|
||||
label: `${runtime}/${sandboxMode}`,
|
||||
};
|
||||
})();
|
||||
|
||||
const updatedAt = entry?.updatedAt;
|
||||
const sessionLine = [
|
||||
`Session: ${args.sessionKey ?? "unknown"}`,
|
||||
typeof updatedAt === "number"
|
||||
? `updated ${formatAge(now - updatedAt)}`
|
||||
: "no activity",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" • ");
|
||||
|
||||
const isGroupSession =
|
||||
entry?.chatType === "group" ||
|
||||
entry?.chatType === "room" ||
|
||||
@@ -229,8 +272,30 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
? (args.groupActivation ?? entry?.groupActivation ?? "mention")
|
||||
: undefined;
|
||||
|
||||
const authMode =
|
||||
args.modelAuth ?? resolveModelAuthMode(provider, args.config);
|
||||
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);
|
||||
const optionParts = [
|
||||
`Runtime: ${runtime.label}`,
|
||||
`Think: ${thinkLevel}`,
|
||||
`Verbose: ${verboseLevel}`,
|
||||
reasoningLevel !== "off" ? `Reasoning: ${reasoningLevel}` : null,
|
||||
`Elevated: ${elevatedLevel}`,
|
||||
];
|
||||
const optionsLine = optionParts.filter(Boolean).join(" · ");
|
||||
const activationParts = [
|
||||
groupActivationValue ? `👥 Activation: ${groupActivationValue}` : null,
|
||||
`🪢 Queue: ${queueMode}${queueDetails}`,
|
||||
];
|
||||
const activationLine = activationParts.filter(Boolean).join(" · ");
|
||||
|
||||
const authMode = resolveModelAuthMode(provider, args.config);
|
||||
const showCost = authMode === "api-key";
|
||||
const costConfig = showCost
|
||||
? resolveModelCostConfig({
|
||||
@@ -253,36 +318,30 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
: undefined;
|
||||
const costLabel = showCost && hasUsage ? formatUsd(cost) : undefined;
|
||||
|
||||
const parts: Array<string | null> = [];
|
||||
parts.push(`status ${args.sessionKey ?? "unknown"}`);
|
||||
|
||||
const modelLabel = model ? `${provider}/${model}` : "unknown";
|
||||
const authLabel = authMode && authMode !== "unknown" ? ` (${authMode})` : "";
|
||||
parts.push(`model ${modelLabel}${authLabel}`);
|
||||
|
||||
const authLabelValue =
|
||||
args.modelAuth ??
|
||||
(authMode && authMode !== "unknown" ? authMode : undefined);
|
||||
const authLabel = authLabelValue ? ` · 🔑 ${authLabelValue}` : "";
|
||||
const modelLine = `🧠 Model: ${modelLabel}${authLabel}`;
|
||||
const commit = resolveCommitHash();
|
||||
const versionLine = `🦞 ClawdBot ${VERSION}${commit ? ` (${commit})` : ""}`;
|
||||
const usagePair = formatUsagePair(inputTokens, outputTokens);
|
||||
if (usagePair) parts.push(usagePair);
|
||||
if (costLabel) parts.push(`cost ${costLabel}`);
|
||||
const costLine = costLabel ? `💵 Cost: ${costLabel}` : null;
|
||||
|
||||
const contextSummary = formatContextUsageShort(
|
||||
totalTokens && totalTokens > 0 ? totalTokens : null,
|
||||
contextTokens ?? null,
|
||||
);
|
||||
parts.push(contextSummary);
|
||||
parts.push(`compactions ${entry?.compactionCount ?? 0}`);
|
||||
parts.push(`think ${thinkLevel}`);
|
||||
parts.push(`verbose ${verboseLevel}`);
|
||||
parts.push(`reasoning ${reasoningLevel}`);
|
||||
parts.push(`elevated ${elevatedLevel}`);
|
||||
if (groupActivationValue) parts.push(`activation ${groupActivationValue}`);
|
||||
|
||||
const queueMode = args.queue?.mode ?? "unknown";
|
||||
const queueDetails = formatQueueDetails(args.queue);
|
||||
parts.push(`queue ${queueMode}${queueDetails}`);
|
||||
|
||||
if (args.usageLine) parts.push(args.usageLine);
|
||||
|
||||
return parts.filter(Boolean).join(" · ");
|
||||
return [
|
||||
versionLine,
|
||||
modelLine,
|
||||
usagePair,
|
||||
costLine,
|
||||
`📚 ${contextLine}`,
|
||||
args.usageLine,
|
||||
`🧵 ${sessionLine}`,
|
||||
`⚙️ ${optionsLine}`,
|
||||
activationLine,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function buildHelpMessage(): string {
|
||||
|
||||
Reference in New Issue
Block a user