galElmalah/claude-queue-plugin
claude-queue
Claude Code 外掛,在回合進行中以 /q 暫存提示,待回合結束後依序送出;可重排、編輯、插隊或一次送出整疊。需要 function hooks 與互動式終端。
關於這個 mod
claude-queue 是以 Claude Code function hooks 實作的 TypeScript 外掛。在 Claude 執行期間輸入 /q <text>,訊息不會進入進行中的回合,而會留在提示框上方的佇列中,待回合結束後依序送出;也可用 /q up|down|mv|rm|edit|clear|send 或佇列上的按鈕重排、編輯、移除與送出。[ ▶ ]//q now <n> 可讓某則訊息搭上目前回合的下一次工具呼叫。
需求:Claude Code 2.1.272 以上、設定 CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1,且需要互動式終端(claude -p 不支援)。安裝可透過 claude plugin marketplace add galElmalah/claude-queue-plugin 後安裝,或以 claude --plugin-dir . 載入本機 clone。
已知限制包含:送出的提示會被引擎標示為外掛訊息、插隊訊息不會顯示 ❯ 列、外掛提示之間至少間隔 5 秒且每個工作階段上限 50 則、Esc 中斷不會阻止佇列送出、僅支援文字,以及滑鼠點擊需要 fullscreen renderer(/tui fullscreen 或 CLAUDE_CODE_NO_FLICKER=1)。採 MIT 授權。
安裝
請先查看作者 README,確認 marketplace 與外掛名稱;指令可能隨儲存庫結構而變動。
claude plugin marketplace add galElmalah/claude-queue-plugin claude plugin install claude-queue
原文 / README
claude-queue
/q <text> while Claude is working: the text does not land in the running
turn. It waits in a stack drawn above the prompt box and goes out once the
turn has ended, one after the other, in the order you queued them — or the
order you put them in, the stack being a thing you can reorder, edit and
prune while the turn runs. Enter alone is untouched: a line typed without
/q goes into the turn the way it always did.
⏺ Reading hooks/register.ts …
╭──────────────────────────────────────────────────────────────────────────╮
│queued · 2 · sent when the turn ends │
│1 also update the README [ ↓ ] [ ▶ ] [ edit ] [ ✕ ] │
│2 then run the e2e suite [ ↑ ] [ ▶ ] [ edit ] [ ✕ ] │
│[ clear ] │
╰──────────────────────────────────────────────────────────────────────────╯
❯
Stock Claude Code delivers a mid-turn message into the turn, beside the
next tool result, so the model reads it halfway through work it has not
finished. A /q line is held instead: the turn ends on the thing it was
asked, and your next thought starts a turn of its own.
[ ▶ ] (and /q now <n>) is the way back in for the one that will not wait:
the entry leaves the stack and rides the running turn's next tool result as
context, the way a mid-turn message arrives. It waits in the band, marked
▶ … waiting for the next tool call, until a tool call carries it, with a
line in the transcript at the press and another when it goes in; a turn
that calls no tool sends it first of all when it ends. A turn that is only
streaming text has no tool call coming: Esc ends it, and the entry goes out
as the next prompt. With nothing running
[ ▶ ] is what it always was: to the front, and out at once.
A mod: a plugin built on Claude Code function hooks, TypeScript that runs inside Claude Code's own process. Early access, so it needs the environment variable below and the API can change between releases.
Requirements
- Claude Code 2.1.272 or later with
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1. - An interactive terminal session. Nothing is held in
claude -p.
Quick start
-
Turn function hooks on, in
~/.claude/settings.json:{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } } -
Load the plugin from a clone:
git clone https://github.com/galElmalah/claude-queue-plugin cd claude-queue-plugin claude --plugin-dir .Or install it, the repository being its own marketplace:
claude plugin marketplace add galElmalah/claude-queue-plugin claude plugin install claude-queue@claude-queue-plugin -
Ask Claude for something slow, then type
/qand your next message and press Enter. It appears in the band instead of in the turn.
Use
| command | what it does |
| --- | --- |
| /q | lists what is held, and ends with the status line |
| /q <text> | holds text until the turn ends; sent at once when nothing runs |
| /q up <n> | down <n> | moves entry n one place |
| /q mv <n> <m> | moves entry n to position m |
| /q now <n> | pushes it into the running turn at its next tool call, or sends it at once when nothing is running |
| /q rm <n> | takes entry n out |
| /q edit <n> | takes it out and puts its text back in the prompt box |
| /q clear | drops the lot |
| /q send | sends now, when the session is idle |
| /q status | turn idle · 2 held · waiting |
/q runs while a turn is in flight, which is the only time the stack fills.
The band does the same things under the mouse:
[ ↑ ] and [ ↓ ] reorder (the row at either end keeps the column and drops
the button it cannot use), [ ▶ ] pushes that one into the turn, [ edit ], [ ✕ ],
and [ send ] and [ clear ] under the stack. No hotkeys: a digit or a
letter would fire while you were typing the next message.
Only the fullscreen renderer sends the band the mouse: /tui fullscreen, or
CLAUDE_CODE_NO_FLICKER=1. On the classic renderer (the default in most
terminals) a click on a button lands nowhere, and the entry goes out when the
turn ends as if nothing had been pressed; the band says so under its buttons.
The keyboard reaches them on either renderer: ctrl+x tab moves the focus
into the band, Tab walks the buttons, Enter presses one — or type /q rm <n>.
[ edit ] turns the row into a text field, prefilled, with Enter to keep what
you typed. The band has to hold the keyboard for that, and only ctrl+x tab
gives it: press that, Tab along to the row's [ edit ], then Enter. A click on
[ edit ] presses it without moving the keyboard off the composer, so there
the text goes back into the prompt box instead, exactly as /q edit <n> does.
Esc gives the keyboard back to the composer without telling the plugin, so the
field stays open, blurred, holding what you typed; the [ ✕ ] beside it closes
it and leaves the entry as it was. The field is one line: on an entry of
several, it edits the first and keeps the rest.
Everything held goes out when the turn ends, an Esc-interrupted turn included —
[ send ] and /q send are there for the rare stack that is still sitting
there.
Options
joined (off by default) sends the whole stack as one prompt when the turn
ends, its entries separated by a blank line, instead of a turn each. Set it
with /plugin configure claude-queue, or in settings.json:
{ "pluginConfigs": { "claude-queue": { "options": { "joined": true } } } }
How it works
hooks/register.ts is the whole plugin.
command.runon/qis the way in:immediate, so it runs while a turn is in flight, and anything after/qthat is not a subcommand is pushed on the stack.prompt.submitis left alone but for reading the turn id off it, for a module reloaded mid-turn.turn.startandturn.completetrack the running turn. Every ending of the main loop's turn drains —abortedtoo — the first entry (or, underjoined, the lot) going out with$.prompt.submit, which runs once the session is idle; the nextturn.completesends the next. A subagent'sturn.completecarries anagentIdand is ignored — it ends inside the session's own turn.tool.callis the steer route.[ ▶ ]moves the entry to a list of its own; the hook awaitsnext(e), and on a main-loop call (noe.agentId) that was not denied it returns the object it got with the framed text appended tocontext— what the model reads after the tool's result and the user never sees. Returning that object keeps core'sref, so the tool's own messages are used verbatim. The pushed text is not a user message: nothing of it enters the transcript history, so$.ui.logwrites the one linequeue: pushed into the turn · …for the person instead.ui.renderofAbovePromptdraws the band inside a rounded dimBox, sized toe.props.bodyColumnsand yielding to a survey. TheButtons'onPressclosures edit the stack and$.ui.invalidate("ui.render"). The same hook is the backstop: a stack drawn while no turn is running by the plugin's own count arms a drain, so nothing sits there. It trusts that count overe.props.isWorking, which reads false while a tool runs under the fullscreen renderer. A submit from inside a hook's dispatch is refused, so every drain goes through$.clock.after(0, …).[ edit ]swaps the row for anInputand asks for the band's focus ring with$.ui.focus. The ring only lands on an element already on screen, so the call is retried a few times over the frame that draws the field; the engine also skips theui.focusevent for a plugin's own call, so the answer to$.ui.focus, not the event, is what says the field has it. Adeny(the band is not holding the keyboard) falls back to$.prompt.fill. Esc raises nothing at all, which is why the open field carries its own[ ✕ ].- The stack is a session's own, and a hot reload of the module empties it.
Limits
- A held prompt arrives as the plugin's, not as yours. The engine frames
every
$.prompt.submitas "The claude-queue plugin sent a message: …", says so to the model, and draws the framing in the transcript row. The text is yours and reaches the model whole; the framing is the engine's, and the plugin cannot redraw that row either — the engine skips a plugin's hooks on anything its own prompt produced. - A pushed entry has no
❯row. It rides a tool result, which the transcript does not show as a message; the plugin writes a dim line at the press (❯ … · into the turn at its next tool call) and one when a tool call carries it. Between the two, the band's▶row is where it is. - The engine spaces a plugin's prompts 5 seconds apart and allows 50 a
session. A stack drains no faster than that: a short reply is followed by a
few idle seconds before the next held prompt goes.
joinedsends the lot as one prompt and pays the wait once. - Context another plugin attached to the held prompt is not carried.
The prompt goes out later as its text alone;
$.prompt.submittakes nocontext. - An Esc does not hold the stack back. The turn it interrupted has ended,
so what was typed behind it goes out. Take it out with
[ ✕ ]or/q clearif the interrupt changed your mind about it too. - Text only.
/qtakes a line of text; an image goes in the turn as it always did. - Clicks need the fullscreen renderer. On the classic renderer the band
draws but never hears the mouse; use ctrl+x tab,
/q rm <n>, or switch with/tui fullscreen. - Terminal only: the band is a terminal surface, and mid-turn typing is a thing only an interactive session does.
Develop
npm run test:e2e # the plugin inside a real Claude Code (below)
npm run typecheck # against .claude/types (run /plugin-types first)
npm run validate # what the engine sees the module hook and call
Typechecking needs the declarations of your Claude Code build: open a session
in the repository root with function hooks on and run /plugin-types, which
writes the git-ignored .claude/types/.
End-to-end tests
tests/e2e drives a real interactive Claude Code in a tmux pane, its replies
scripted by aimock and paced so a turn
takes about fifteen seconds, which is the room the tests type into. It covers
an idle prompt passing through, a plain line typed mid-turn left to the engine,
one and two /q lines held over a turn and the order they come back in, /q rm, /q edit, /q up, /q down, /q mv,
/q now on a text-only turn and on one that calls a tool (a fixture that
answers only when the pushed text is in the request), /q status, the field
a row's [ edit ] opens, clicks on [ ✕ ] and [ ↓ ], a turn ended with Esc draining anyway, and joined.
Needs tmux and claude on PATH, and the checkout to be a folder Claude Code
trusts; skipped otherwise. About four minutes.
Mouse reports only reach the band under the fullscreen renderer
(CLAUDE_CODE_NO_FLICKER=1), so the click tests have a session of their own.
The keyboard reaches it in either: ctrl+x tab, then Tab along the row.
Edits to hooks/ hot-reload into a running --plugin-dir session, which
empties the stack. Start Claude with --debug-file /tmp/q.log to see what
the engine refused.
License
MIT.
