diff options
| -rw-r--r-- | pi/agent/extensions/loop-scheduler/index.ts | 80 |
1 files changed, 17 insertions, 63 deletions
diff --git a/pi/agent/extensions/loop-scheduler/index.ts b/pi/agent/extensions/loop-scheduler/index.ts index c6b9c27..ec7c9bb 100644 --- a/pi/agent/extensions/loop-scheduler/index.ts +++ b/pi/agent/extensions/loop-scheduler/index.ts @@ -419,12 +419,15 @@ export default function loopSchedulerExtension(pi: ExtensionAPI): void { pi.registerCommand("loop", { description: "Schedule a recurring prompt: /loop 10m <prompt>, /loop list, /loop cancel <id|all>, /loop <preset-name>", - // Provide autocomplete for subcommands and preset names loaded from loop-presets.md. - // "cancel" and "preset" subcommands expand directly into their third-level completions - // so the user never gets stuck with just the verb and no further suggestions. + // Provide autocomplete for subcommands and preset names. + // + // CRITICAL: pi's autocomplete.js line 209 does: + // if (!argumentSuggestions || argumentSuggestions.length === 0) return null; + // …and a null return from getSuggestions causes the TUI to fall back to filesystem + // completion. Every branch here must return at least one item to prevent that. getArgumentCompletions: (prefix: string) => { - // cancel/rm/delete <id|all>: expand to full "cancel all" / "cancel <job-id>" items - // as soon as the prefix matches the verb (with or without trailing space/partial id). + // cancel/rm/delete <id|all>: expand to "cancel all" + active job IDs as soon as + // the prefix matches the verb. Falls back to showing "cancel all" if no jobs exist. if (/^(cancel|rm|delete)(\s+\S*)?$/i.test(prefix)) { const verb = prefix.split(/\s+/)[0]!; const partial = (prefix.match(/^(?:cancel|rm|delete)\s+(\S*)$/i)?.[1] ?? "").toLowerCase(); @@ -441,12 +444,13 @@ export default function loopSchedulerExtension(pi: ExtensionAPI): void { }); } } - return results.length > 0 ? results : null; + // Always return at least one item — empty results would fall back to filesystem. + return results.length > 0 ? results : [{ value: `${verb} all`, label: `${verb} all`, description: "Cancel all active jobs" }]; } - // preset <name>: expand directly to "preset <name>" completions as soon as - // the prefix matches "preset" (with or without trailing space/partial name), - // so the user never hits a dead end at the bare verb. + // preset <name>: expand to "preset <name>" items matching the partial name. + // If the presets file is missing or empty, surface the edit suggestion so the + // user gets a useful hint rather than filesystem completion. if (/^preset(\s+\S*)?$/i.test(prefix)) { const partial = (prefix.match(/^preset\s+(\S*)$/i)?.[1] ?? "").toLowerCase(); const results = loadPresets() @@ -456,7 +460,8 @@ export default function loopSchedulerExtension(pi: ExtensionAPI): void { label: `preset ${p.name}`, description: `every ${p.intervalLabel} — ${shortenPrompt(p.prompt, 50)}`, })); - return results.length > 0 ? results : null; + // Always return at least one item to prevent filesystem fallback. + return results.length > 0 ? results : [{ value: "edit", label: "edit", description: `No presets found — edit ${PRESETS_FILE}` }]; } // Top-level: subcommand stubs and direct preset name shortcuts. @@ -476,7 +481,8 @@ export default function loopSchedulerExtension(pi: ExtensionAPI): void { if (!prefix) return all; const lower = prefix.toLowerCase(); const filtered = all.filter((item) => item.value.startsWith(lower)); - return filtered.length > 0 ? filtered : null; + // Return fixed list as fallback rather than null, so filesystem completion never fires. + return filtered.length > 0 ? filtered : all; }, handler: async (args, ctx) => { rememberContext(ctx); @@ -602,58 +608,6 @@ export default function loopSchedulerExtension(pi: ExtensionAPI): void { }, }); - // Separate command for running named presets with reliable first-argument autocomplete. - // This avoids relying on multi-word prefix matching in /loop's getArgumentCompletions. - pi.registerCommand("loop-preset", { - description: "Activate a named loop preset: /loop-preset <name>. Use /loop presets to list.", - getArgumentCompletions: (prefix: string) => { - const lower = prefix.toLowerCase(); - const items = loadPresets().map((p) => ({ - value: p.name, - label: p.name, - description: `every ${p.intervalLabel} — ${shortenPrompt(p.prompt, 50)}`, - })); - if (!prefix) return items; - const filtered = items.filter((item) => item.value.startsWith(lower)); - return filtered.length > 0 ? filtered : []; - }, - handler: async (args, ctx) => { - rememberContext(ctx); - - if (!ctx.hasUI) { - writeCommandOutput("The /loop-preset command requires an interactive or RPC session that stays open."); - return; - } - - const name = args.trim(); - if (!name) { - notify(formatPresetList(), "info", ctx); - return; - } - - const preset = lookupPreset(name); - if (!preset) { - notify(`No preset named '${name}'. 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, - ); - }, - }); - pi.on("session_start", async (_event, ctx) => { rememberContext(ctx); agentBusy = false; |
