fix(coding-agent/session): restricted queued-message editor restore to user-authored messages
- Added `isUserQueuedMessage()` to `agent-session.ts`, treating only plain user turns and visible `attribution: "user"` custom messages (e.g. `/skill`) as restorable, so advisor concern/blocker notes, hidden goal/plan/budget steers, and IRC/extension asides no longer leak into the editor on Esc/Alt+Up. - Reworked `clearQueue()` to return only user-authored messages while re-queuing advisor cards via `replaceQueues()` (so the user-interrupt abort path still re-records them as advice) and dropping other agent-authored steers to prevent a silent auto-resume on leftover internal context. - Filtered `getQueuedMessages()` chips and rewrote `popLastQueuedMessage()` to skip agent-authored cards and pull the last user-authored entry, while `queuedMessageCount` still counts all displayable queued work. - Extended `input-controller-skill-queue.test.ts` with a `queueAdvisorSteer` helper and cases asserting advisor/IRC cards count as pending work but stay out of chips, restore, and `popLastQueuedMessage`, and survive `clearQueue()`.
This commit is contained in:
@@ -225,6 +225,7 @@ function queueCustomSteer(session: AgentSession, chip: string, content = "skill
|
||||
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
||||
content,
|
||||
display: true,
|
||||
attribution: "user",
|
||||
details: {
|
||||
name: "foo",
|
||||
path: "/s.md",
|
||||
@@ -236,6 +237,18 @@ function queueCustomSteer(session: AgentSession, chip: string, content = "skill
|
||||
});
|
||||
}
|
||||
|
||||
function queueAdvisorSteer(session: AgentSession, note = "consider X"): void {
|
||||
session.agent.steer({
|
||||
role: "custom",
|
||||
customType: "advisor",
|
||||
content: `Advisor:\n- [blocker] ${note}`,
|
||||
display: true,
|
||||
attribution: "agent",
|
||||
details: { notes: [{ note, severity: "blocker" }] },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
describe("AgentSession derived queued custom display", () => {
|
||||
let fixture: SessionFixture | undefined;
|
||||
|
||||
@@ -259,7 +272,7 @@ describe("AgentSession derived queued custom display", () => {
|
||||
expect(session.queuedMessageCount).toBe(1);
|
||||
});
|
||||
|
||||
it("excludes display-suppressed custom messages from pending chips and counts", async () => {
|
||||
it("excludes display-suppressed custom messages from chips/count and never restores them", async () => {
|
||||
fixture = await createRealSession();
|
||||
const { session } = fixture;
|
||||
session.agent.steer({
|
||||
@@ -273,6 +286,35 @@ describe("AgentSession derived queued custom display", () => {
|
||||
|
||||
expect(session.getQueuedMessages().steering).toEqual([]);
|
||||
expect(session.queuedMessageCount).toBe(0);
|
||||
// A hidden agent-authored steer must not leak into the editor, and clearQueue
|
||||
// must drop it so abort()'s stranded-message drain can't auto-resume the run
|
||||
// the user just interrupted (the drain gate is agent.hasQueuedMessages()).
|
||||
expect(session.clearQueue().steering).toEqual([]);
|
||||
expect(session.agent.hasQueuedMessages()).toBe(false);
|
||||
});
|
||||
|
||||
it("never restores a visible agent-authored custom steer, and drops it on clearQueue", async () => {
|
||||
fixture = await createRealSession();
|
||||
const { session } = fixture;
|
||||
// An IRC aside / extension notice: visible, but agent-authored — editing it
|
||||
// makes no sense, so it must not ride the Esc/Alt+Up editor-restore path.
|
||||
session.agent.steer({
|
||||
role: "custom",
|
||||
customType: "irc",
|
||||
content: "peer pinged you",
|
||||
display: true,
|
||||
attribution: "agent",
|
||||
details: {},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
expect(session.getQueuedMessages().steering).toEqual([]);
|
||||
// popLast leaves the agent steer untouched (not user-restorable)...
|
||||
expect(session.popLastQueuedMessage()).toBeUndefined();
|
||||
expect(session.agent.peekSteeringQueue()).toHaveLength(1);
|
||||
// ...and clearQueue restores nothing while dropping it (no auto-resume leftover).
|
||||
expect(session.clearQueue().steering).toEqual([]);
|
||||
expect(session.agent.hasQueuedMessages()).toBe(false);
|
||||
});
|
||||
|
||||
it("popLastQueuedMessage restores chip text and removes the core queue entry", async () => {
|
||||
@@ -283,6 +325,52 @@ describe("AgentSession derived queued custom display", () => {
|
||||
expect(session.popLastQueuedMessage()?.text).toBe("/skill:foo bar");
|
||||
expect(session.getQueuedMessages().steering).toEqual([]);
|
||||
});
|
||||
|
||||
it("counts a queued advisor card as pending work but keeps it out of chips and restore", async () => {
|
||||
fixture = await createRealSession();
|
||||
const { session } = fixture;
|
||||
queueAdvisorSteer(session, "guard the null path");
|
||||
|
||||
// Advisor cards are real pending work (feeds hasPendingMessages/empty-Enter abort)...
|
||||
expect(session.queuedMessageCount).toBe(1);
|
||||
// ...but are never editable user input.
|
||||
expect(session.getQueuedMessages().steering).toEqual([]);
|
||||
|
||||
// clearQueue must not surface the advisor note for editor restore, and must
|
||||
// leave the card queued so the abort/resume path still delivers it.
|
||||
const cleared = session.clearQueue();
|
||||
expect(cleared.steering).toEqual([]);
|
||||
expect(cleared.followUp).toEqual([]);
|
||||
expect(session.agent.peekSteeringQueue()).toHaveLength(1);
|
||||
expect(session.popLastQueuedMessage()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clearQueue restores user messages but preserves a queued advisor card", async () => {
|
||||
fixture = await createRealSession();
|
||||
const { session } = fixture;
|
||||
queueCustomSteer(session, "/skill:foo bar");
|
||||
queueAdvisorSteer(session, "rename the symbol");
|
||||
|
||||
const cleared = session.clearQueue();
|
||||
expect(cleared.steering).toEqual([{ text: "/skill:foo bar", images: undefined }]);
|
||||
// The advisor card survives in the agent-core queue; the user's message left.
|
||||
const remaining = session.agent.peekSteeringQueue();
|
||||
expect(remaining).toHaveLength(1);
|
||||
expect(remaining[0]).toMatchObject({ customType: "advisor" });
|
||||
});
|
||||
|
||||
it("popLastQueuedMessage steps over an advisor card to the user message", async () => {
|
||||
fixture = await createRealSession();
|
||||
const { session } = fixture;
|
||||
queueCustomSteer(session, "/skill:foo bar");
|
||||
queueAdvisorSteer(session, "watch the race");
|
||||
|
||||
expect(session.popLastQueuedMessage()?.text).toBe("/skill:foo bar");
|
||||
// Advisor card remains queued, not restored.
|
||||
const remaining = session.agent.peekSteeringQueue();
|
||||
expect(remaining).toHaveLength(1);
|
||||
expect(remaining[0]).toMatchObject({ customType: "advisor" });
|
||||
});
|
||||
});
|
||||
|
||||
function createStubInteractiveModeContextForUiHelpers(session: AgentSession) {
|
||||
|
||||
Reference in New Issue
Block a user