Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@
},
"defaultServiceTier": {
"type": "string"
},
"verbose": {
"type": "boolean"
},
"verboseMaxEvents": {
"type": "number",
"minimum": 1
},
"verboseFlushMs": {
"type": "number",
"minimum": 250
}
}
},
Expand Down Expand Up @@ -100,6 +111,18 @@
"defaultServiceTier": {
"label": "Default Service Tier",
"advanced": true
},
"verbose": {
"label": "Verbose Progress",
"help": "Send short progress updates while Codex is reasoning, using tools, or running commands."
},
"verboseMaxEvents": {
"label": "Verbose Max Events",
"advanced": true
},
"verboseFlushMs": {
"label": "Verbose Flush Delay (ms)",
"advanced": true
}
}
}
9 changes: 9 additions & 0 deletions src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ describe("CodexAppServerClient.setThreadModel", () => {
command: "codex",
args: [],
requestTimeoutMs: 1_000,
verbose: false,
verboseMaxEvents: 12,
verboseFlushMs: 2_500,
},
{
debug: vi.fn(),
Expand Down Expand Up @@ -253,6 +256,9 @@ describe("CodexAppServerClient.setThreadPermissions", () => {
command: "codex",
args: [],
requestTimeoutMs: 1_000,
verbose: false,
verboseMaxEvents: 12,
verboseFlushMs: 2_500,
},
{
debug: vi.fn(),
Expand Down Expand Up @@ -320,6 +326,9 @@ describe("CodexAppServerClient.startReview", () => {
command: "codex",
args: [],
requestTimeoutMs: 1_000,
verbose: false,
verboseMaxEvents: 12,
verboseFlushMs: 2_500,
},
{
debug: vi.fn(),
Expand Down
93 changes: 93 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type CompactProgress,
type CompactResult,
type ContextUsageSnapshot,
type CodexProgressEvent,
type CodexTurnInputItem,
type ExperimentalFeatureSummary,
type McpServerSummary,
Expand Down Expand Up @@ -2026,6 +2027,84 @@ function extractAssistantNotificationText(
return { mode: "ignore", text: "" };
}

function extractProgressEventFromItem(params: unknown): CodexProgressEvent | undefined {
const item = asRecord(asRecord(params)?.item) ?? asRecord(params);
if (!item) {
return undefined;
}
const itemId = pickString(item, ["id", "itemId", "item_id"]);
const itemType = pickString(item, ["type"])?.trim();
const normalizedType = itemType?.toLowerCase();
const keyPrefix = itemId || normalizedType || "item";
switch (normalizedType) {
case "reasoning":
return { label: "Reasoning", key: `reasoning:${keyPrefix}` };
case "commandexecution":
return { label: "Command", key: `command:${keyPrefix}` };
case "mcptoolcall": {
return { label: "Tool", key: `mcp:${keyPrefix}` };
}
case "dynamictoolcall": {
return { label: "Tool", key: `dynamic:${keyPrefix}` };
}
case "collabagenttoolcall": {
return { label: "Agent", key: `agent:${keyPrefix}` };
}
case "websearch": {
return { label: "Web search", key: `web:${keyPrefix}` };
}
case "filechange":
return { label: "File edit", key: `file:${keyPrefix}` };
case "imageview":
return { label: "Image view", key: `image-view:${keyPrefix}` };
case "imagegeneration":
return { label: "Image generation", key: `image-generation:${keyPrefix}` };
case "contextcompaction":
return { label: "Compacting context", key: `compact:${keyPrefix}` };
default:
return undefined;
}
}

function extractProgressEventFromNotification(
methodLower: string,
params: unknown,
): CodexProgressEvent | undefined {
if (methodLower === "turn/started") {
return { label: "Working", key: "turn:started" };
}
if (methodLower === "item/started") {
return extractProgressEventFromItem(params);
}
if (
methodLower === "item/reasoning/textdelta" ||
methodLower === "item/reasoning/summarytextdelta" ||
methodLower === "item/reasoning/summarypartadded"
) {
const ids = extractIds(params);
return { label: "Reasoning", key: `reasoning:${ids.itemId ?? ids.runId ?? "delta"}` };
}
if (methodLower === "item/mcptoolcall/progress") {
const ids = extractIds(params);
return {
label: "Tool",
key: `mcp-progress:${ids.itemId ?? ids.runId ?? "progress"}`,
};
}
if (methodLower === "command/exec/outputdelta") {
return { label: "Command output", key: "command-output" };
}
if (methodLower === "item/commandexecution/outputdelta") {
const ids = extractIds(params);
return { label: "Command output", key: `command-output:${ids.itemId ?? ids.runId ?? "item"}` };
}
if (methodLower === "turn/plan/updated" || methodLower === "item/plan/delta") {
const ids = extractIds(params);
return { label: "Planning", key: `planning:${ids.itemId ?? ids.runId ?? "turn"}` };
}
return undefined;
}

function extractPlanDeltaNotification(value: unknown): { itemId?: string; delta: string } {
return {
itemId: extractAssistantItemId(value),
Expand Down Expand Up @@ -3263,6 +3342,7 @@ export class CodexAppServerClient {
collaborationMode?: CollaborationMode;
onPendingInput?: (state: PendingInputState | null) => Promise<void> | void;
onFileEdits?: (text: string) => Promise<void> | void;
onProgress?: (event: CodexProgressEvent) => Promise<void> | void;
onInterrupted?: () => Promise<void> | void;
}): ActiveCodexRun {
let threadId = params.existingThreadId?.trim() || "";
Expand All @@ -3284,6 +3364,18 @@ export class CodexAppServerClient {
let terminalError: TurnTerminalError | undefined;
let approvalCancelled = false;
let notificationQueue = Promise.resolve();
let lastProgressKey = "";
const emitProgress = async (event: CodexProgressEvent | undefined) => {
if (!event || !params.onProgress) {
return;
}
const key = event.key ?? `${event.label}:${event.detail ?? ""}`;
if (key === lastProgressKey) {
return;
}
lastProgressKey = key;
await params.onProgress(event);
};
const pendingInputCoordinator = createPendingInputCoordinator({
onPendingInput: params.onPendingInput,
onActivated: () => {
Expand Down Expand Up @@ -3321,6 +3413,7 @@ export class CodexAppServerClient {
}
threadId ||= ids.threadId ?? "";
turnId ||= ids.runId ?? "";
await emitProgress(extractProgressEventFromNotification(methodLower, notificationParams));
const tokenUsage = extractThreadTokenUsageSnapshot(notificationParams);
if (tokenUsage) {
latestContextUsage = tokenUsage;
Expand Down
1 change: 1 addition & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const COMMANDS = [
["cas_fast", "Toggle or inspect fast mode for the current Codex binding."],
["cas_model", "List or switch the Codex model for the current binding."],
["cas_permissions", "Show Codex permissions and account status."],
["cas_verbose", "Toggle or inspect verbose progress updates."],
["cas_init", "Forward /init to Codex."],
["cas_diff", "Forward /diff to Codex."],
["cas_rename", "Rename the Codex thread and optionally sync the conversation name."],
Expand Down
20 changes: 20 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ function readNumber(
return fallback;
}

function readBoolean(record: Record<string, unknown>, key: string, fallback: boolean): boolean {
const value = record[key];
if (typeof value === "boolean") {
return value;
}
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (normalized === "true") {
return true;
}
if (normalized === "false") {
return false;
}
}
return fallback;
}

export function resolvePluginSettings(rawConfig: unknown): PluginSettings {
const record = asRecord(rawConfig);
const transport = record.transport === "websocket" ? "websocket" : "stdio";
Expand All @@ -82,6 +99,9 @@ export function resolvePluginSettings(rawConfig: unknown): PluginSettings {
defaultWorkspaceDir: readString(record, "defaultWorkspaceDir"),
defaultModel: readString(record, "defaultModel"),
defaultServiceTier: readString(record, "defaultServiceTier"),
verbose: readBoolean(record, "verbose", false),
verboseMaxEvents: readNumber(record, "verboseMaxEvents", 12, 1),
verboseFlushMs: readNumber(record, "verboseFlushMs", 2_500, 250),
};
}

Expand Down
31 changes: 22 additions & 9 deletions src/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,13 @@ const discordSdkState = vi.hoisted(() => ({
resolveDiscordAccount: vi.fn(() => ({ accountId: "default" })),
}));

const telegramSdkState = vi.hoisted(() => ({
resolveTelegramAccount: vi.fn(() => ({ accountId: "default", token: "telegram-token" })),
}));

vi.mock("openclaw/plugin-sdk/discord", () => ({
buildDiscordComponentMessage: discordSdkState.buildDiscordComponentMessage,
editDiscordComponentMessage: discordSdkState.editDiscordComponentMessage,
registerBuiltDiscordComponentMessage: discordSdkState.registerBuiltDiscordComponentMessage,
resolveDiscordAccount: discordSdkState.resolveDiscordAccount,
}));

vi.mock("openclaw/plugin-sdk/telegram-account", () => ({
resolveTelegramAccount: telegramSdkState.resolveTelegramAccount,
}));

function makeStateDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-app-server-test-"));
}
Expand Down Expand Up @@ -552,7 +544,6 @@ beforeEach(() => {
discordSdkState.editDiscordComponentMessage.mockClear();
discordSdkState.registerBuiltDiscordComponentMessage.mockClear();
discordSdkState.resolveDiscordAccount.mockClear();
telegramSdkState.resolveTelegramAccount.mockClear();
vi.spyOn(CodexAppServerClient.prototype, "logStartupProbe").mockResolvedValue();
vi.stubGlobal(
"fetch",
Expand Down Expand Up @@ -756,6 +747,28 @@ describe("Discord controller flows", () => {
expect(planUsage).toEqual({ text: "Usage: /cas_plan <goal> | /cas_plan off" });
});

it("toggles verbose progress through cas_verbose", async () => {
const { controller } = await createControllerHarness();

const enabled = await controller.handleCommand("cas_verbose", buildTelegramCommandContext({
args: "on",
commandBody: "/cas_verbose on",
}));
const status = await controller.handleCommand("cas_verbose", buildTelegramCommandContext({
args: "status",
commandBody: "/cas_verbose status",
}));
const disabled = await controller.handleCommand("cas_verbose", buildTelegramCommandContext({
args: "off",
commandBody: "/cas_verbose off",
}));

expect(enabled).toEqual({ text: "Codex verbose progress is on." });
expect(status).toEqual({ text: "Codex verbose progress is on. Source: runtime override." });
expect(disabled).toEqual({ text: "Codex verbose progress is off." });
expect((controller as any).store.getVerboseOverride()).toBe(false);
});

it("offers a New button on /cas_resume and flips into the new-thread project picker", async () => {
const { controller } = await createControllerHarness();

Expand Down
Loading