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:
can1357
2026-06-16 16:05:36 +02:00
parent bbb41a3c93
commit 623e9d3710
2 changed files with 144 additions and 9 deletions
@@ -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) {