From 147a295ecd6577530fe87d32f72baf9a73231f68 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 25 Mar 2026 21:35:48 +0200 Subject: loop-scheduler: add third-level autocomplete for cancel and preset subcommands - /loop cancel now suggests "all" and active job IDs - /loop preset new explicit subcommand with third-level name autocomplete - Second-level completions now also include "preset" as a discoverable subcommand Co-Authored-By: Claude Sonnet 4.6 --- pi/agent/extensions/loop-scheduler/index.ts | 63 ++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) (limited to 'pi') diff --git a/pi/agent/extensions/loop-scheduler/index.ts b/pi/agent/extensions/loop-scheduler/index.ts index 75f2ae1..a5bad6e 100644 --- a/pi/agent/extensions/loop-scheduler/index.ts +++ b/pi/agent/extensions/loop-scheduler/index.ts @@ -419,10 +419,46 @@ export default function loopSchedulerExtension(pi: ExtensionAPI): void { description: "Schedule a recurring prompt: /loop 10m , /loop list, /loop cancel , /loop ", // Provide autocomplete for subcommands and preset names loaded from loop-presets.md. + // Third-level completions (after a subcommand word) are handled before the top-level list. getArgumentCompletions: (prefix: string) => { + // Third-level: /loop cancel|rm|delete + // Suggest "all" and any active job IDs matching the partial input. + const cancelMatch = prefix.match(/^(cancel|rm|delete)\s+(\S*)$/i); + if (cancelMatch) { + const verb = cancelMatch[1]!; + const partial = (cancelMatch[2] ?? "").toLowerCase(); + const results = []; + if ("all".startsWith(partial)) { + results.push({ value: `${verb} all`, label: "all", description: "Cancel all active jobs" }); + } + for (const job of jobs.values()) { + if (job.id.startsWith(partial)) { + results.push({ value: `${verb} ${job.id}`, label: job.id, description: shortenPrompt(job.prompt, 50) }); + } + } + return results.length > 0 ? results : null; + } + + // Third-level: /loop preset + // Suggest preset names from loop-presets.md. + const presetMatch = prefix.match(/^preset\s+(\S*)$/i); + if (presetMatch) { + const partial = (presetMatch[1] ?? "").toLowerCase(); + const results = loadPresets() + .filter((p) => p.name.startsWith(partial)) + .map((p) => ({ + value: `preset ${p.name}`, + label: p.name, + description: `every ${p.intervalLabel} — ${shortenPrompt(p.prompt, 50)}`, + })); + return results.length > 0 ? results : null; + } + + // Second-level: subcommand names and direct preset names. const fixed = [ { value: "list", label: "list", description: "Show active loop jobs" }, { value: "cancel", label: "cancel", description: "Cancel: cancel " }, + { value: "preset", label: "preset", description: "Activate a named preset: preset " }, { value: "edit", label: "edit", description: "Edit presets file in $EDITOR" }, { value: "presets", label: "presets", description: "List available presets" }, ]; @@ -448,7 +484,7 @@ export default function loopSchedulerExtension(pi: ExtensionAPI): void { const trimmed = args.trim(); if (!trimmed || trimmed.toLowerCase() === "help") { notify( - "Usage: /loop | /loop | /loop list | /loop cancel | /loop edit | /loop presets | /loop ", + "Usage: /loop | /loop | /loop list | /loop cancel | /loop edit | /loop presets | /loop preset | /loop ", "info", ctx, ); @@ -495,6 +531,31 @@ export default function loopSchedulerExtension(pi: ExtensionAPI): void { return; } + // Explicit "preset " subcommand — mirrors the single-word shorthand but more + // discoverable and supports autocomplete at the third level. + const presetCmd = trimmed.match(/^preset\s+(\S+)$/i); + if (presetCmd) { + const preset = lookupPreset(presetCmd[1]!); + if (!preset) { + notify(`No preset named '${presetCmd[1]}'. Use /loop presets to list available presets.`, "warning", ctx); + return; + } + if (jobs.size >= MAX_JOBS) { + notify(`Too many active loop jobs (${jobs.size}). Cancel one first.`, "warning", ctx); + return; + } + const job = createJob(preset.prompt, preset.intervalMs, preset.intervalLabel); + jobs.set(job.id, job); + scheduleJobTimer(job); + updateUi(ctx); + notify( + `Scheduled loop ${job.id} [${preset.name}] every ${job.intervalLabel}: ${shortenPrompt(job.prompt)}`, + "success", + ctx, + ); + return; + } + // If the argument is a single word (no spaces), check if it matches a preset name. // Note: a preset named "hourly" or "daily" takes precedence over the interval shorthand. if (!/\s/.test(trimmed)) { -- cgit v1.2.3