You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request
When working with codex, I often submit the same request repeatedly in a loop that captures any learnings and updates an ADR to keep steering itself towards an outcome that I define up front. This allows me to queue up work to run overnight or when I step away from the keyboard which is useful. When I tried to do the same in opencode, it appeared that the messages I sent were queued immediately and interrupted work in progress in a way that prevented me from queuing up many messages to step away. Can you think of a way to queue a message that would wait until opencode was waiting for user input (rather than interrupting a current task)?
I have not tested this (ai generated so consider it pseudo code) but it should work and achieve what you want in a better way than queuing the same message repeatedly:
/**
* RepeatPromptPlugin for OpenCode
*
* Adds two control commands (typed as normal chat messages or as custom commands):
*
* 1) /loop [N] "prompt"
* - Starts an automated loop that repeatedly sends the same prompt.
* - Optional N: maximum number of iterations (e.g., /loop 10 "Update the ADR...").
* If no number is provided, the loop runs indefinitely until stopped.
* - The plugin only sends the next iteration AFTER the assistant finishes responding
* (it waits for the `session.idle` event, so it won't interrupt ongoing generation).
* - The loop automatically stops if the assistant’s most recent response contains:
* </STOP_LOOP>
* You can include logic in your prompt to emit that token when done.
*
* 2) /loopstop
* - Immediately stops any active loop for the current session.
* - Best-effort aborts any in-progress generation.
* - Shows a UI toast confirmation ("Loop stopped.").
*
* Notes:
* - If you send manual messages while a loop is active, they’re processed normally.
* The loop will continue on the next `session.idle` unless you stop it.
*/
import type { Plugin } from "@opencode-ai/plugin";
type LoopState = {
prompt: string;
maxLoops: number; // Infinity = run until stopped
sent: number; // how many times we've sent the loop prompt
waitingForIdle: boolean; // true after we dispatch, until we see session.idle again
stopKeyword: string;
};
export const RepeatPromptPlugin: Plugin = async ({ client }) => {
const STOP_KEYWORD = "</STOP_LOOP>";
const loops = new Map<string, LoopState>();
function getPayload(evt: any): any {
return evt?.properties ?? evt?.data ?? evt?.payload ?? {};
}
function getSessionId(evt: any): string | null {
const p = getPayload(evt);
return (
p.session?.id ??
p.sessionId ??
p.sessionID ??
p.session_id ??
p.session ??
null
);
}
async function toast(
message: string,
variant: "info" | "success" | "error" = "info",
) {
try {
await client.tui.showToast({ body: { message, variant } });
} catch {
// Ignore if no TUI is connected
}
}
function stripQuotes(s: string): string {
const t = s.trim();
if (t.length >= 2) {
const a = t[0];
const b = t[t.length - 1];
if (
(a === `"` && b === `"`) ||
(a === `'` && b === `'`) ||
(a === "`" && b === "`")
) {
return t.slice(1, -1);
}
}
return t;
}
function parseLoopCommandText(
text: string,
):
| { kind: "loop"; maxLoops: number; prompt: string }
| { kind: "loopstop" }
| { kind: "error"; message: string }
| null {
const t = text.trim();
// Important: check /loopstop BEFORE /loop (since /loopstop starts with /loop)
if (/^\/loopstop\b/.test(t)) return { kind: "loopstop" };
if (!/^\/loop\b/.test(t)) return null;
const rest = t.replace(/^\/loop\b/, "").trim();
if (!rest) {
return { kind: "error", message: `Usage: /loop [N] "prompt"` };
}
// Optional leading integer
const m = rest.match(/^(\d+)\s+([\s\S]+)$/);
if (m) {
const n = Number(m[1]);
if (!Number.isFinite(n) || n <= 0) {
return { kind: "error", message: "Loop count must be a positive integer." };
}
return { kind: "loop", maxLoops: n, prompt: stripQuotes(m[2]) };
}
return { kind: "loop", maxLoops: Infinity, prompt: stripQuotes(rest) };
}
function partsToText(parts: any[]): string {
const out: string[] = [];
for (const part of parts ?? []) {
if (!part) continue;
if (typeof part === "string") {
out.push(part);
continue;
}
const t =
(typeof part.text === "string" && part.text) ||
(typeof part.content === "string" && part.content) ||
(typeof part.value === "string" && part.value) ||
"";
if (t) out.push(t);
}
return out.join("");
}
async function lastAssistantText(sessionId: string): Promise<string> {
try {
const msgs = (await client.session.messages({ path: { id: sessionId } })) as any[];
for (let i = msgs.length - 1; i >= 0; i--) {
const row = msgs[i];
const info = row?.info ?? row?.message ?? row;
const role = info?.role ?? info?.type;
if (role === "assistant") {
return partsToText(row?.parts ?? info?.parts ?? []);
}
}
return "";
} catch {
return "";
}
}
async function stopLoop(sessionId: string, reason: string) {
loops.delete(sessionId);
await toast(reason, "info");
}
async function startLoop(sessionId: string, maxLoops: number, prompt: string) {
if (!prompt.trim()) {
await toast("Loop not started: prompt is empty.", "error");
return;
}
loops.set(sessionId, {
prompt,
maxLoops,
sent: 0,
waitingForIdle: false,
stopKeyword: STOP_KEYWORD,
});
await toast(
maxLoops === Infinity ? "Loop started (∞)." : `Loop started (${maxLoops}).`,
"success",
);
// Kick off immediately (if session is busy, we’ll retry on idle)
void maybeSendNext(sessionId);
}
async function maybeSendNext(sessionId: string) {
const st = loops.get(sessionId);
if (!st) return;
if (st.waitingForIdle) return;
if (st.sent >= st.maxLoops) {
await stopLoop(sessionId, "Loop finished (max iterations reached).");
return;
}
// Check stop keyword in the latest assistant output before sending again
const last = await lastAssistantText(sessionId);
if (last.includes(st.stopKeyword)) {
await stopLoop(sessionId, "Loop stopped (stop keyword detected).");
return;
}
st.sent += 1;
st.waitingForIdle = true;
try {
await client.session.prompt({
path: { id: sessionId },
body: { parts: [{ type: "text", text: st.prompt }] },
});
} catch (err) {
st.sent -= 1;
st.waitingForIdle = false;
await toast(
`Loop send failed; will retry on idle. (${(err as any)?.message ?? String(err)})`,
"error",
);
}
}
return {
event: async ({ event }) => {
const type = event.type;
const sessionId = getSessionId(event);
if (!sessionId) return;
if (type === "message.updated") {
const p = getPayload(event);
const container = p.message ?? p;
const info = container.info ?? container;
const role = info.role ?? info.type;
let text = "";
if (typeof container.content === "string") text = container.content;
else if (typeof info.content === "string") text = info.content;
else if (Array.isArray(container.parts)) text = partsToText(container.parts);
else if (Array.isArray(p.parts)) text = partsToText(p.parts);
if (!text) return;
// Stop keyword (assistant)
if (role === "assistant" && text.includes(STOP_KEYWORD)) {
await stopLoop(sessionId, "Loop stopped (stop keyword detected).");
return;
}
// Control commands (user)
if (role === "user") {
const parsed = parseLoopCommandText(text);
if (!parsed) return;
if (parsed.kind === "error") {
await toast(parsed.message, "error");
return;
}
if (parsed.kind === "loopstop") {
if (loops.has(sessionId)) {
loops.delete(sessionId);
try {
await client.session.abort({ path: { id: sessionId } });
} catch {}
await toast("Loop stopped.", "success");
} else {
await toast("No active loop to stop.", "info");
}
return;
}
if (parsed.kind === "loop") {
// Best-effort: prevent the assistant from responding to the control message itself
try {
await client.session.abort({ path: { id: sessionId } });
} catch {}
await startLoop(sessionId, parsed.maxLoops, parsed.prompt);
return;
}
}
return;
}
if (type === "session.idle") {
const st = loops.get(sessionId);
if (!st) return;
st.waitingForIdle = false;
if (st.sent >= st.maxLoops) {
await stopLoop(sessionId, "Loop finished (max iterations reached).");
return;
}
void maybeSendNext(sessionId);
return;
}
// Optional: if your OpenCode emits command events, support them too
if (type === "command.executed" || type === "tui.command.execute") {
const p = getPayload(event);
const cmd = String(p.command ?? p.name ?? "");
const args = Array.isArray(p.args) ? p.args.map(String).join(" ") : "";
if (cmd === "loop") {
const fake = `/loop ${args}`.trim();
const parsed = parseLoopCommandText(fake);
if (parsed && parsed.kind === "loop") {
await startLoop(sessionId, parsed.maxLoops, parsed.prompt);
}
} else if (cmd === "loopstop") {
await stopLoop(sessionId, "Loop stopped.");
}
}
},
};
};
@jaresty one aspect is immediate to-queue backgrounding: what would you say if the system offered not just to put your background task to the queue but persist it - one way to store task query is for example the beads (bd) plugin.
However, currently I don't see a way how to make opencode process such stored queue autonomously - on explicit request or just in the background as it idles. I can imagine lots of tiny non-critical tasks which could add up a lot on codebase quality for example, or allowing create independent experimental features you maybe wouldn't like to merge immediately.
Yes I think background processing on idle is what I've been looking for. For now, the workaround I've been using is to, in my prompt, specify how many times I want it to execute. That seems to work pretty well with the caveat that eventually I run out of context so some kind of automatic compaction would be really helpful then.
there are people with opinions on both fronts, do you all think itd be good to offer this as a configuration option? Like we can keep default the same but do a codex like queuing instead if u have certain config set?
It would be nice if you could switch; I.e. sometimes queue for idle and other times send as soon as possible. I have found cases where sending sooner is useful too.
Reacted by Art Shendrik, Javohir Ismatullayev, David Torbeck, Jacob Daitzman, Michael Bøcker-Larsen, paw-share-club, Dyllon Gunawardhana, Savina Weerasooriya, Sherif Abdel-Naby and rpdeshaies
Could be also that actual requirement is to have an off-TUI background daemon to take care of repos to do chores of predictable complexity and with high success rate even with some reiterations (dynamic programming alike).
The demand for unattended work goes then beyond TUI detaching to CICD integration like the Renovate Bot does.
Could be also that actual requirement is to have an off-TUI background daemon to take care of repos to do chores of predictable complexity and with high success rate even with some reiterations (dynamic programming alike).
The demand for unattended work goes then beyond TUI detaching to CICD integration like the Renovate Bot does.
This also seems useful, but I'm not currently prepared to dramatically change my workflow and I'm not certain if I can in my current enterprise environment. I had been quite happy with codex's job handling but I like opencode's UI better mostly so I'm mostly using this now but it is limiting what I can do because of the job handling.
It would be very helpful to have this as a command, something like /queue to setup a message that gets sent when the agent finishes. The use case for this is quite different for immediately sent messages, which I use to steer the ongoing work. Queue use case would be to send a follow up task to the agent, or to ask it if it's really done or just providing a summary report of what was done.
Reacted by Art Shendrik, Octane, paw-share-club and maggie44
Codex just pushed an experimental feature to shift default behavior to steering, or mid-turn queueing via openai/codex#9077, which aligns with opencode. However, they maintain a hotkey for tail-queuing so that both use-cases remain available (vs a either/or config). For opencode that may look like:
Enter → queue immediate
Alt + Enter → queue at end of agent loop
Reacted by Art Shendrik, Junjie, Octane, Andy Yao, Benoit de Chezelles, David Torbeck, Nick Breaton, Jan-Henrik Damaschke, Emanuel André Medina Araujo, Sherif Abdel-Naby and 1 more
Codex just pushed an experimental feature to shift default behavior to steering, or mid-turn queueing via openai/codex#9077, which aligns with opencode. However, they maintain a hotkey for tail-queuing so that both use-cases remain available (vs a either/or config). For opencode that may look like:
* `Enter` → queue immediate
* `Alt + Enter` → queue at end of agent loop
This would be awesome if it is added to opencode, same as in the Codex App.
Reacted by mathe00, Michael Bøcker-Larsen, 陈杨文 and Ty Zes
I think @itpropro's suggestion is the best. However, while waiting for it to be implemented, I'm doing what @sepsi77 says with a /queue command using a plugin.
A minimal fix for this is up in #44117 — it restores the General → Follow-up behavior (Queue/Steer) setting that was disabled in ae7e2eb, which un-blocks the queue machinery that still ships in the app. If you'd like this landed, a 👍 on the PR helps it survive the automated PR cleanup and get maintainer review.
OpenCode v2 has a delayed queue next to the default steering behavior:
Enter steers: the message is delivered at the next step boundary while the agent is still working.
ctrl+x return (prompt.queue) queues: the message waits until the agent is idle and never interrupts current work.
Queued messages are delivered one at a time. The agent finishes the first one, goes idle, and only then gets the next, so you can line up several prompts and step away. While a message is pending you can also move it between queue and steer, or delete it.
Added in #40922; the keybind moved to command IDs in #41882.
Closing as completed. If something doesn't work on v2, please open a new issue.
Activity
github-actions commentedon Dec 11, 2025
This issue might be a duplicate of existing issues. Please check:
Feel free to ignore if none of these address your specific case.
tiagoefreitas commentedon Dec 13, 2025
I have not tested this (ai generated so consider it pseudo code) but it should work and achieve what you want in a better way than queuing the same message repeatedly:
`
synergiator commentedon Dec 21, 2025
@jaresty one aspect is immediate to-queue backgrounding: what would you say if the system offered not just to put your background task to the queue but persist it - one way to store task query is for example the beads (bd) plugin.
#5895
However, currently I don't see a way how to make opencode process such stored queue autonomously - on explicit request or just in the background as it idles. I can imagine lots of tiny non-critical tasks which could add up a lot on codebase quality for example, or allowing create independent experimental features you maybe wouldn't like to merge immediately.
jaresty commentedon Dec 21, 2025
Yes I think background processing on idle is what I've been looking for. For now, the workaround I've been using is to, in my prompt, specify how many times I want it to execute. That seems to work pretty well with the caveat that eventually I run out of context so some kind of automatic compaction would be really helpful then.
rekram1-node commentedon Dec 21, 2025
there are people with opinions on both fronts, do you all think itd be good to offer this as a configuration option? Like we can keep default the same but do a codex like queuing instead if u have certain config set?
jaresty commentedon Dec 21, 2025
It would be nice if you could switch; I.e. sometimes queue for idle and other times send as soon as possible. I have found cases where sending sooner is useful too.
synergiator commentedon Dec 22, 2025
Could be also that actual requirement is to have an off-TUI background daemon to take care of repos to do chores of predictable complexity and with high success rate even with some reiterations (dynamic programming alike).
The demand for unattended work goes then beyond TUI detaching to CICD integration like the Renovate Bot does.
gitlab.com/nagyv/gitlab-opencode
jaresty commentedon Dec 23, 2025
This also seems useful, but I'm not currently prepared to dramatically change my workflow and I'm not certain if I can in my current enterprise environment. I had been quite happy with codex's job handling but I like opencode's UI better mostly so I'm mostly using this now but it is limiting what I can do because of the job handling.
sepsi77 commentedon Jan 7, 2026
It would be very helpful to have this as a command, something like /queue to setup a message that gets sent when the agent finishes. The use case for this is quite different for immediately sent messages, which I use to steer the ongoing work. Queue use case would be to send a follow up task to the agent, or to ask it if it's really done or just providing a summary report of what was done.
bndnsmth commentedon Jan 16, 2026
Codex just pushed an experimental feature to shift default behavior to steering, or mid-turn queueing via openai/codex#9077, which aligns with opencode. However, they maintain a hotkey for tail-queuing so that both use-cases remain available (vs a either/or config). For opencode that may look like:
Enter→ queue immediateAlt + Enter→ queue at end of agent loopitpropro commentedon Feb 12, 2026
This would be awesome if it is added to opencode, same as in the Codex App.
mathe00 commentedon Feb 12, 2026
I think @itpropro's suggestion is the best. However, while waiting for it to be implemented, I'm doing what @sepsi77 says with a /queue command using a plugin.
itpropro commentedon Mar 24, 2026
Any updates on this?
distbit0 commentedon Apr 4, 2026
would be v useful. i am missing not having it after switching from codex
cowboycodr commentedon Apr 5, 2026
I'm definitely in support of an enter to regularly queue the message, and alt + enter for the delayed queue.
Or something of the sorts.
wanton7 commentedon Apr 15, 2026
Pi also uses ALT+Enter to push messages into delayed queue. https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent
mirsella commentedon Apr 15, 2026
even 5.4 xhigh required multiples prompt to get a working plugin, and no one shared theirs, so here's mine https://github.com/mirsella/opencode-queue
edit: improved the plugin a lot, there new features and its really stable and working well :) easy install and usage too
Ewal11 commentedon Apr 28, 2026
Would love to see codex style queuing
kevin91nl commentedon May 8, 2026
Codex/VSCode style queuing would be great
Dmitrii-Software-Testing commentedon Aug 3, 2026
Agree, very important feature!
chaosmanage commentedon Aug 22, 2026
A minimal fix for this is up in #44117 — it restores the General → Follow-up behavior (Queue/Steer) setting that was disabled in ae7e2eb, which un-blocks the queue machinery that still ships in the app. If you'd like this landed, a 👍 on the PR helps it survive the automated PR cleanup and get maintainer review.
neriousy commentedon Oct 1, 2026
OpenCode v2 has a delayed queue next to the default steering behavior:
ctrl+x return(prompt.queue) queues: the message waits until the agent is idle and never interrupts current work.Queued messages are delivered one at a time. The agent finishes the first one, goes idle, and only then gets the next, so you can line up several prompts and step away. While a message is pending you can also move it between queue and steer, or delete it.
Added in #40922; the keybind moved to command IDs in #41882.
Closing as completed. If something doesn't work on v2, please open a new issue.