[FEATURE]: Delayed queue feature #3485

Open
opened 2026-02-16 17:40:18 -05:00 by yindo · 12 comments
Owner

Originally created by @jaresty on GitHub (Dec 11, 2025).

Feature hasn't been suggested before.

  • 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)?

Originally created by @jaresty on GitHub (Dec 11, 2025). ### Feature hasn't been suggested before. - [x] 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)?
yindo added the discussion label 2026-02-16 17:40:18 -05:00
Author
Owner

@github-actions[bot] commented on GitHub (Dec 11, 2025):

This issue might be a duplicate of existing issues. Please check:

  • #5333: [FEATURE]: Graceful handling of queued messages after session interrupt - addresses message queueing and task interruption handling
  • #3098: Chained prompts executing together instead of one after the other - discusses prompt/message execution sequencing
  • #4821: [FEATURE]: Add ability to unqueue messages - related queue management feature request

Feel free to ignore if none of these address your specific case.

@github-actions[bot] commented on GitHub (Dec 11, 2025): This issue might be a duplicate of existing issues. Please check: - #5333: [FEATURE]: Graceful handling of queued messages after session interrupt - addresses message queueing and task interruption handling - #3098: Chained prompts executing together instead of one after the other - discusses prompt/message execution sequencing - #4821: [FEATURE]: Add ability to unqueue messages - related queue management feature request Feel free to ignore if none of these address your specific case.
Author
Owner

@tiagoefreitas commented on GitHub (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:

/**
 * 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.");
        }
      }
    },
  };
};

`​​​​​​​​​​​​​​​​​​​​​​​​​​​​

@tiagoefreitas commented on GitHub (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: ``` /** * 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."); } } }, }; }; ``` `​​​​​​​​​​​​​​​​​​​​​​​​​​​​
Author
Owner

@synergiator commented on GitHub (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.

https://github.com/sst/opencode/issues/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.

@synergiator commented on GitHub (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. https://github.com/sst/opencode/issues/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.
Author
Owner

@jaresty commented on GitHub (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.

@jaresty commented on GitHub (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.
Author
Owner

@rekram1-node commented on GitHub (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?

@rekram1-node commented on GitHub (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?
Author
Owner

@jaresty commented on GitHub (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.

@jaresty commented on GitHub (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.
Author
Owner

@synergiator commented on GitHub (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

@synergiator commented on GitHub (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](https://gitlab.com/nagyv/gitlab-opencode)
Author
Owner

@jaresty commented on GitHub (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

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.

@jaresty commented on GitHub (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](https://gitlab.com/nagyv/gitlab-opencode) > > 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.
Author
Owner

@sepsi77 commented on GitHub (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.

@sepsi77 commented on GitHub (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.
Author
Owner

@brandon93s commented on GitHub (Jan 16, 2026):

Codex just pushed an experimental feature to shift default behavior to steering, or mid-turn queueing via https://github.com/openai/codex/pull/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
@brandon93s commented on GitHub (Jan 16, 2026): Codex just pushed an experimental feature to shift default behavior to steering, or mid-turn queueing via https://github.com/openai/codex/pull/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
Author
Owner

@itpropro commented on GitHub (Feb 12, 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 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.

@itpropro commented on GitHub (Feb 12, 2026): > Codex just pushed an experimental feature to shift default behavior to steering, or mid-turn queueing via [openai/codex#9077](https://github.com/openai/codex/pull/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.
Author
Owner

@mathe00 commented on GitHub (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.

@mathe00 commented on GitHub (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.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#3485