deliver the whole queue as one message, not one turn each
Queued prompts are almost always one thought arriving in pieces — a correction, then the thing you forgot. Answering them one turn at a time made the agent reply to the first without knowing the second existed, then re-answer once it did. Joined with a blank line between, in the order written, which is how they read anyway. The tray is unchanged: they stay separate rows, each removable right up until they go. What merges is the delivery, not the queue. Only a lone prompt can still be a slash command. Joined to anything else it is text that happens to start with a slash, and running it as a command would silently drop everything queued behind it. The drain now takes the whole queue at once, so it loops twice at most — again only if the batch was a handled command and something arrived while it ran. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -122,16 +122,28 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
|
||||
};
|
||||
|
||||
/**
|
||||
* Hand one prompt to the agent. Returns whether it actually started a turn — a slash command the client
|
||||
* handled itself (`/clear`, `/model`) never reaches the agent, so the drain below must keep going
|
||||
* rather than wait for a completion that will not come.
|
||||
* Hand a batch to the agent as ONE message. Returns whether it actually started a turn — a slash
|
||||
* command the client handled itself (`/clear`, `/model`) never reaches the agent, so the drain below
|
||||
* must keep going rather than wait for a completion that will not come.
|
||||
*
|
||||
* Attachments are captured when the prompt is composed, not here, so a queued message keeps the files
|
||||
* that were on it rather than picking up whatever happens to be attached when its turn arrives.
|
||||
* Several queued prompts become one message rather than one turn each. They are almost always one
|
||||
* thought arriving in pieces — a correction, then the thing you forgot — and answering them separately
|
||||
* makes the agent reply to the first without knowing the second exists, then re-answer with it. Joined
|
||||
* with a blank line between, in the order they were written, which is how they read.
|
||||
*
|
||||
* Attachments are captured when each prompt is composed, not here, so a queued message keeps the files
|
||||
* that were on it; the batch carries all of them together.
|
||||
*/
|
||||
const deliverPrompt = async (item: QueuedPrompt): Promise<boolean> => {
|
||||
const text = item.raw.trim();
|
||||
if (text.startsWith('/')) {
|
||||
const deliverBatch = async (items: QueuedPrompt[]): Promise<boolean> => {
|
||||
const text = items
|
||||
.map((item) => item.raw.trim())
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
if (!text) return false;
|
||||
|
||||
// Only a lone prompt can be a slash command. Joined to anything else it is text that happens to start
|
||||
// with a slash, and running it as a command would silently drop everything queued behind it.
|
||||
if (items.length === 1 && text.startsWith('/')) {
|
||||
const result = await slashCommands.execute(text);
|
||||
if (result.handled) {
|
||||
setCommandFeedback(result.feedback);
|
||||
@@ -140,15 +152,19 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
|
||||
}
|
||||
setCommandFeedback(null);
|
||||
|
||||
const prefix = items.map((item) => item.prefix).join('');
|
||||
const ids = items.flatMap((item) => item.ids);
|
||||
const images = items.flatMap((item) => item.images);
|
||||
|
||||
let prompt = promptPrefix ? `${promptPrefix}\n\n${text}` : text;
|
||||
if (item.prefix) prompt = `${item.prefix}${prompt}`;
|
||||
if (prefix) prompt = `${prefix}${prompt}`;
|
||||
|
||||
// Send the cwd on every message (not just the first): OpenCode rebuilds its working-directory
|
||||
// system prompt each turn, so it needs the cwd every time. It stays constant for a session.
|
||||
sendPrompt(
|
||||
prompt,
|
||||
!sessionId && item.ids.length > 0 ? item.ids : undefined,
|
||||
item.images.length > 0 ? item.images : undefined,
|
||||
!sessionId && ids.length > 0 ? ids : undefined,
|
||||
images.length > 0 ? images : undefined,
|
||||
cwd,
|
||||
undefined,
|
||||
thinkingLevel,
|
||||
@@ -158,11 +174,13 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
|
||||
return true;
|
||||
};
|
||||
|
||||
// Takes the whole queue at once, so the loop runs twice at most: again only if the batch turned out to
|
||||
// be a handled slash command (no turn started) and something was queued while it ran.
|
||||
const drainQueue = async () => {
|
||||
while (queuedRef.current.length > 0) {
|
||||
const [next, ...rest] = queuedRef.current;
|
||||
setQueue(rest);
|
||||
if (await deliverPrompt(next!)) return; // a turn is running; its completion drains the rest
|
||||
const batch = queuedRef.current;
|
||||
setQueue([]);
|
||||
if (await deliverBatch(batch)) return;
|
||||
}
|
||||
};
|
||||
// The completion effect is declared above this, and must call the current one rather than the closure
|
||||
@@ -200,7 +218,7 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
|
||||
setQueue([...queuedRef.current, item]);
|
||||
return;
|
||||
}
|
||||
await deliverPrompt(item);
|
||||
await deliverBatch([item]);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user