diff --git a/packages/coding-agent/src/modes/skill-command.ts b/packages/coding-agent/src/modes/skill-command.ts index 151292563..984fa110b 100644 --- a/packages/coding-agent/src/modes/skill-command.ts +++ b/packages/coding-agent/src/modes/skill-command.ts @@ -26,6 +26,7 @@ interface ParsedSkillCommand { interface InvokeSkillCommandOptions { propagateErrors?: boolean; + queueOnly?: boolean; } /** Built custom-message payload and delivery options for a `/skill:` command. */ @@ -97,7 +98,8 @@ export async function invokeSkillCommandFromText( try { const built = await buildSkillCommandPrompt(ctx, text, streamingBehavior); if (!built) return false; - await ctx.session.promptCustomMessage(built.message, built.options); + const promptOptions = options?.queueOnly ? { ...built.options, queueOnly: true } : built.options; + await ctx.session.promptCustomMessage(built.message, promptOptions); return true; } catch (err) { if (options?.propagateErrors) { diff --git a/packages/coding-agent/src/modes/utils/ui-helpers.ts b/packages/coding-agent/src/modes/utils/ui-helpers.ts index 33dec7ec7..4a831944d 100644 --- a/packages/coding-agent/src/modes/utils/ui-helpers.ts +++ b/packages/coding-agent/src/modes/utils/ui-helpers.ts @@ -668,7 +668,12 @@ export class UiHelpers { } async #deliverQueuedMessage(message: CompactionQueuedMessage): Promise { - if (await invokeSkillCommandFromText(this.ctx, message.text, message.mode, { propagateErrors: true })) { + if ( + await invokeSkillCommandFromText(this.ctx, message.text, message.mode, { + propagateErrors: true, + queueOnly: true, + }) + ) { return; } if (this.ctx.isKnownSlashCommand(message.text)) { diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index de2683f53..a0cb1f1fa 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -6526,7 +6526,10 @@ export class AgentSession { async promptCustomMessage( message: Pick, "customType" | "content" | "display" | "details" | "attribution">, - options?: Pick & { queueChipText?: string }, + options?: Pick & { + queueChipText?: string; + queueOnly?: boolean; + }, ): Promise { const textContent = typeof message.content === "string" @@ -6546,6 +6549,16 @@ export class AgentSession { keywordNotices = this.#createMagicKeywordNotices(skillArgs); } + if (options?.queueOnly) { + if (!options.streamingBehavior) { + throw new AgentBusyError(); + } + for (const notice of keywordNotices) { + await this.#queueCustomMessage(notice, options.streamingBehavior); + } + await this.#queueCustomMessage(message, options.streamingBehavior, options.queueChipText); + return; + } if (this.isStreaming) { if (!options?.streamingBehavior) { throw new AgentBusyError(); @@ -7111,6 +7124,40 @@ export class AgentSession { } } + /** Queue a custom message without starting a turn, matching steer/follow-up delivery. */ + async #queueCustomMessage( + message: Pick, "customType" | "content" | "display" | "details" | "attribution">, + deliverAs: "steer" | "followUp", + queueChipText?: string, + ): Promise { + const details = + queueChipText !== undefined + ? ({ + ...((message.details && typeof message.details === "object" ? message.details : {}) as Record< + string, + unknown + >), + __queueChipText: queueChipText, + } as T) + : message.details; + const appMessage: CustomMessage = { + role: "custom", + customType: message.customType, + content: message.content, + display: message.display, + details, + attribution: message.attribution ?? "agent", + timestamp: Date.now(), + }; + const normalizedAppMessage = await this.#normalizeAgentMessageImages(appMessage); + if (deliverAs === "followUp") { + this.agent.followUp(normalizedAppMessage); + } else { + this.agent.steer(normalizedAppMessage); + } + this.#scheduleIdleQueueDrain(); + } + /** * Send a custom message to the session. Creates a CustomMessageEntry. * diff --git a/packages/coding-agent/test/input-controller-skill-queue.test.ts b/packages/coding-agent/test/input-controller-skill-queue.test.ts index a00185b38..dbe8a899b 100644 --- a/packages/coding-agent/test/input-controller-skill-queue.test.ts +++ b/packages/coding-agent/test/input-controller-skill-queue.test.ts @@ -42,7 +42,7 @@ type PromptCustomMessage = Mock< attribution?: string; details: SkillPromptDetails; }, - options?: { streamingBehavior?: "steer" | "followUp"; queueChipText?: string }, + options?: { streamingBehavior?: "steer" | "followUp"; queueChipText?: string; queueOnly?: boolean }, ) => Promise >; @@ -288,6 +288,24 @@ describe("compaction skill re-invocation", () => { expect(steer).not.toHaveBeenCalled(); expect(followUp).not.toHaveBeenCalled(); }); + + it("queues retry-drained skills without appending them to session history", async () => { + const fixture = await createRealSession(); + try { + const { ctx } = createCompactionDrainContext([{ text: "/skill:test-skill retry args", mode: "followUp" }]); + ctx.session = fixture.session; + const uiHelpers = new UiHelpers(ctx); + + await uiHelpers.flushCompactionQueue({ willRetry: true }); + + expect(fixture.session.getQueuedMessages().followUp).toEqual(["/skill:test-skill retry args"]); + expect(fixture.session.messages).toEqual([]); + } finally { + await fixture.session.dispose(); + fixture.authStorage.close(); + fixture.tempDir.removeSync(); + } + }); }); interface SessionFixture {