|
1 | 1 | import crypto from "node:crypto"; |
2 | 2 | import { resolveAgentDir, resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime"; |
| 3 | +import type { MessagePresentation } from "openclaw/plugin-sdk/interactive-runtime"; |
3 | 4 | import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime"; |
4 | 5 | import type { PluginCommandContext, PluginCommandResult } from "openclaw/plugin-sdk/plugin-entry"; |
5 | 6 | import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; |
@@ -240,12 +241,144 @@ export function resetCodexDiagnosticsFeedbackStateForTests(): void { |
240 | 241 | pendingCodexDiagnosticsConfirmationTokensByScope.clear(); |
241 | 242 | } |
242 | 243 |
|
| 244 | +type CodexPickerButton = { label: string; command: string }; |
| 245 | + |
| 246 | +function buildPickerPresentation(title: string, prompt: string, buttons: CodexPickerButton[]) { |
| 247 | + return { |
| 248 | + title, |
| 249 | + blocks: [ |
| 250 | + { type: "text", text: prompt }, |
| 251 | + { |
| 252 | + type: "buttons", |
| 253 | + buttons: buttons.map((button) => ({ |
| 254 | + label: button.label, |
| 255 | + value: button.command, |
| 256 | + })), |
| 257 | + }, |
| 258 | + ], |
| 259 | + } satisfies MessagePresentation; |
| 260 | +} |
| 261 | + |
| 262 | +/** |
| 263 | + * No-arg `/codex` picker. Core owns the native command tree; channels render |
| 264 | + * the portable buttons as inline controls when their transport can. |
| 265 | + */ |
| 266 | +function buildCodexSubcommandPickerReply(): PluginCommandResult { |
| 267 | + const verbs: CodexPickerButton[] = [ |
| 268 | + { label: "plugins", command: "/codex plugins menu" }, |
| 269 | + { label: "permissions", command: "/codex permissions menu" }, |
| 270 | + { label: "fast", command: "/codex fast menu" }, |
| 271 | + { label: "computer-use", command: "/codex computer-use menu" }, |
| 272 | + { label: "account", command: "/codex account" }, |
| 273 | + { label: "help", command: "/codex help" }, |
| 274 | + ]; |
| 275 | + |
| 276 | + const fallbackTextLines = [ |
| 277 | + "Codex commands. Pick a category or type:", |
| 278 | + "", |
| 279 | + ...verbs.map((v, i) => ` ${i + 1}. ${v.command}`), |
| 280 | + "", |
| 281 | + "Tap 'help' (or type /codex help) for the full list of typeable verbs", |
| 282 | + "including threads, mcp, binding, detach, skills, resume, bind, steer,", |
| 283 | + "model, diagnostics, compact, review, computer-use.", |
| 284 | + "", |
| 285 | + "Top-level shortcuts cover everyday operations: /status, /fast, /help, /stop, /models.", |
| 286 | + ]; |
| 287 | + |
| 288 | + return { |
| 289 | + text: fallbackTextLines.join("\n"), |
| 290 | + presentation: buildPickerPresentation("Codex commands", "Pick a Codex subcommand:", verbs), |
| 291 | + }; |
| 292 | +} |
| 293 | + |
| 294 | +/** Sub-picker for `/codex fast menu` (on / off / status). */ |
| 295 | +function buildCodexFastMenuReply(): PluginCommandResult { |
| 296 | + const modes = ["on", "off", "status"] as const; |
| 297 | + const buttons: CodexPickerButton[] = [ |
| 298 | + ...modes.map((mode) => ({ label: mode, command: `/codex fast ${mode}` })), |
| 299 | + { label: "back", command: "/codex" }, |
| 300 | + ]; |
| 301 | + const fallbackTextLines = [ |
| 302 | + "Codex fast mode. Pick one or type /codex fast <mode>:", |
| 303 | + "", |
| 304 | + ...modes.map((m, i) => ` ${i + 1}. /codex fast ${m}`), |
| 305 | + "", |
| 306 | + "Type '/codex' to go back to the main menu.", |
| 307 | + ]; |
| 308 | + return { |
| 309 | + text: fallbackTextLines.join("\n"), |
| 310 | + presentation: buildPickerPresentation("Codex fast mode", "Pick a Codex fast mode:", buttons), |
| 311 | + }; |
| 312 | +} |
| 313 | + |
| 314 | +/** Sub-picker for `/codex permissions menu` (default / yolo / status). */ |
| 315 | +function buildCodexPermissionsMenuReply(): PluginCommandResult { |
| 316 | + const modes = ["default", "yolo", "status"] as const; |
| 317 | + const buttons: CodexPickerButton[] = [ |
| 318 | + ...modes.map((mode) => ({ label: mode, command: `/codex permissions ${mode}` })), |
| 319 | + { label: "back", command: "/codex" }, |
| 320 | + ]; |
| 321 | + const fallbackTextLines = [ |
| 322 | + "Codex permissions. Pick one or type /codex permissions <mode>:", |
| 323 | + "", |
| 324 | + ...modes.map((m, i) => ` ${i + 1}. /codex permissions ${m}`), |
| 325 | + "", |
| 326 | + "Type '/codex' to go back to the main menu.", |
| 327 | + ]; |
| 328 | + return { |
| 329 | + text: fallbackTextLines.join("\n"), |
| 330 | + presentation: buildPickerPresentation( |
| 331 | + "Codex permissions", |
| 332 | + "Pick a Codex permissions mode:", |
| 333 | + buttons, |
| 334 | + ), |
| 335 | + }; |
| 336 | +} |
| 337 | + |
| 338 | +/** Sub-picker for `/codex computer-use menu` (status / install). */ |
| 339 | +function buildCodexComputerUseMenuReply(): PluginCommandResult { |
| 340 | + const actions = ["status", "install"] as const; |
| 341 | + const buttons: CodexPickerButton[] = [ |
| 342 | + ...actions.map((action) => ({ |
| 343 | + label: action, |
| 344 | + command: `/codex computer-use ${action}`, |
| 345 | + })), |
| 346 | + { label: "back", command: "/codex" }, |
| 347 | + ]; |
| 348 | + const fallbackTextLines = [ |
| 349 | + "Codex computer-use. Pick one or type /codex computer-use <action>:", |
| 350 | + "", |
| 351 | + ...actions.map((a, i) => ` ${i + 1}. /codex computer-use ${a}`), |
| 352 | + "", |
| 353 | + "Flag-driven invocations (--source, --marketplace-path, --marketplace) are not in the picker. Type '/codex computer-use' or read '/codex help' for the full surface.", |
| 354 | + "", |
| 355 | + "Type '/codex' to go back to the main menu.", |
| 356 | + ]; |
| 357 | + return { |
| 358 | + text: fallbackTextLines.join("\n"), |
| 359 | + presentation: buildPickerPresentation( |
| 360 | + "Codex computer-use", |
| 361 | + "Pick a Codex computer-use action:", |
| 362 | + buttons, |
| 363 | + ), |
| 364 | + }; |
| 365 | +} |
| 366 | + |
| 367 | +/** Returns true when the rest-args are exactly `["menu"]` (case-insensitive). */ |
| 368 | +function isMenuVerb(rest: readonly string[]): boolean { |
| 369 | + return rest.length === 1 && (rest[0] ?? "").trim().toLowerCase() === "menu"; |
| 370 | +} |
| 371 | + |
243 | 372 | export async function handleCodexSubcommand( |
244 | 373 | ctx: PluginCommandContext, |
245 | 374 | options: { pluginConfig?: unknown; deps?: Partial<CodexCommandDeps> }, |
246 | 375 | ): Promise<PluginCommandResult> { |
247 | 376 | const deps: CodexCommandDeps = { ...defaultCodexCommandDeps, ...options.deps }; |
248 | | - const [subcommand = "status", ...rest] = splitArgs(ctx.args); |
| 377 | + const args = splitArgs(ctx.args); |
| 378 | + if (args.length === 0) { |
| 379 | + return buildCodexSubcommandPickerReply(); |
| 380 | + } |
| 381 | + const [subcommand = "status", ...rest] = args; |
249 | 382 | const normalized = subcommand.toLowerCase(); |
250 | 383 | if (normalized === "help") { |
251 | 384 | return { text: buildHelp() }; |
@@ -321,9 +454,15 @@ export async function handleCodexSubcommand( |
321 | 454 | return { text: await setConversationModel(deps, ctx, options.pluginConfig, rest) }; |
322 | 455 | } |
323 | 456 | if (normalized === "fast") { |
| 457 | + if (isMenuVerb(rest)) { |
| 458 | + return buildCodexFastMenuReply(); |
| 459 | + } |
324 | 460 | return { text: await setConversationFastMode(deps, ctx, options.pluginConfig, rest) }; |
325 | 461 | } |
326 | 462 | if (normalized === "permissions") { |
| 463 | + if (isMenuVerb(rest)) { |
| 464 | + return buildCodexPermissionsMenuReply(); |
| 465 | + } |
327 | 466 | return { text: await setConversationPermissions(deps, ctx, options.pluginConfig, rest) }; |
328 | 467 | } |
329 | 468 | if (normalized === "compact") { |
@@ -360,6 +499,9 @@ export async function handleCodexSubcommand( |
360 | 499 | ); |
361 | 500 | } |
362 | 501 | if (normalized === "computer-use" || normalized === "computeruse") { |
| 502 | + if (isMenuVerb(rest)) { |
| 503 | + return buildCodexComputerUseMenuReply(); |
| 504 | + } |
363 | 505 | return { |
364 | 506 | text: await handleComputerUseCommand(deps, options.pluginConfig, rest), |
365 | 507 | }; |
|
0 commit comments