From f6d69620f96aabfc65c980c9baad3d6b0fed24d7 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 14 Jun 2026 08:38:59 +0900 Subject: [PATCH] fix(coding-agent): restore compaction-queued messages on Alt+Up dequeue restoreQueuedMessagesToEditor only drained the agent steering/follow-up queue via session.clearQueue(), but the "Alt+Up to edit" pending-bar hint is rendered for both that queue and ctx.compactionQueuedMessages. Messages typed while the session was compacting -- including /skill:* follow-ups, which the follow-up path routes to the compaction queue before its skill check -- were advertised by the hint yet unreachable, so Alt+Up reported "No queued messages to restore". Drain compactionQueuedMessages alongside the agent queue, merged in the same order the pending bar renders (session-steer, compaction-steer, session-follow-up, compaction-follow-up). The existing text-join, image hand-back, and abort paths operate on the merged list unchanged. --- packages/coding-agent/CHANGELOG.md | 1 + .../src/modes/controllers/input-controller.ts | 15 ++++++- .../input-controller-compaction-image.test.ts | 41 ++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 6f1d1b374..d3c2bbf17 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed session JSONL persistence so the first assistant turn materializes the file synchronously, leaves the append writer open, and writes later entries with a sync append writer even during writer-close races instead of waiting on a queued rewrite. +- Fixed `Alt+Up` (dequeue) reporting "No queued messages to restore" for messages — including skills — typed while the session was compacting. `restoreQueuedMessagesToEditor` now drains `compactionQueuedMessages` alongside the agent queue, so the `Alt+Up to edit` hint restores every pending message it advertises. ## [15.12.5] - 2026-06-13 ### Changed diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index e56b34cc5..8b3022f62 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -924,7 +924,20 @@ export class InputController { restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number { this.ctx.locallySubmittedUserSignatures.clear(); const { steering, followUp } = this.ctx.session.clearQueue(); - const allQueued = [...steering, ...followUp]; + // Messages typed while compacting live in `compactionQueuedMessages`, not the + // agent queue `clearQueue()` drains — but the pending bar shows the same + // "Alt+Up to edit" hint for them (ui-helpers `updatePendingMessagesDisplay`). + // Drain them here too so the dequeue restores every message the hint + // advertises; otherwise a skill/text queued during compaction is stranded and + // Alt+Up reports "No queued messages to restore". + const compactionQueued = this.ctx.compactionQueuedMessages; + this.ctx.compactionQueuedMessages = []; + const allQueued = [ + ...steering, + ...compactionQueued.filter(e => e.mode === "steer").map(e => ({ text: e.text, images: e.images })), + ...followUp, + ...compactionQueued.filter(e => e.mode === "followUp").map(e => ({ text: e.text, images: e.images })), + ]; if (allQueued.length === 0) { this.ctx.updatePendingMessagesDisplay(); if (options?.abort) { diff --git a/packages/coding-agent/test/input-controller-compaction-image.test.ts b/packages/coding-agent/test/input-controller-compaction-image.test.ts index 14526b109..64466253c 100644 --- a/packages/coding-agent/test/input-controller-compaction-image.test.ts +++ b/packages/coding-agent/test/input-controller-compaction-image.test.ts @@ -17,6 +17,7 @@ import { beforeAll, describe, expect, mock, test } from "bun:test"; import type { ImageContent } from "@oh-my-pi/pi-ai"; +import { InputController } from "@oh-my-pi/pi-coding-agent/modes/controllers/input-controller"; import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; import type { CompactionQueuedMessage, InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types"; import { UiHelpers } from "@oh-my-pi/pi-coding-agent/modes/utils/ui-helpers"; @@ -50,6 +51,8 @@ function makeCtx(initialQueue: CompactionQueuedMessage[] = []) { }), }; + let editorText = ""; + const ctx = { session, compactionQueuedMessages: [...initialQueue], @@ -58,8 +61,10 @@ function makeCtx(initialQueue: CompactionQueuedMessage[] = []) { pendingMessagesContainer: { clear: () => {}, addChild: () => {}, removeChild: () => {} }, editor: { addToHistory: () => {}, - setText: () => {}, - getText: () => "", + setText: (text: string) => { + editorText = text; + }, + getText: () => editorText, imageLinks: undefined as (string | undefined)[] | undefined, }, keybindings: { getDisplayString: () => "Alt+Up" }, @@ -127,3 +132,35 @@ describe("compaction queue image forwarding", () => { expect(followUpCalls).toEqual([{ text: "and this one", images: [image] }]); }); }); + +describe("compaction queue Alt+Up restore", () => { + test("restoreQueuedMessagesToEditor drains a compaction-queued skill", () => { + const { ctx } = makeCtx([{ text: "/skill:foo bar", mode: "followUp", images: undefined }]); + const restored = new InputController(ctx).restoreQueuedMessagesToEditor(); + expect(restored).toBe(1); + expect(ctx.editor.getText()).toBe("/skill:foo bar"); + expect(ctx.compactionQueuedMessages).toEqual([]); + }); + + test("restored compaction images return to the pending-image buffer", () => { + const image = img("YmF6"); + const { ctx } = makeCtx([{ text: "look", mode: "steer", images: [image] }]); + const restored = new InputController(ctx).restoreQueuedMessagesToEditor(); + expect(restored).toBe(1); + expect(ctx.pendingImages).toEqual([image]); + }); + + test("session and compaction queues restore in pending-bar order", () => { + const { ctx, session } = makeCtx([ + { text: "compaction steer", mode: "steer", images: undefined }, + { text: "compaction followup", mode: "followUp", images: undefined }, + ]); + (session as unknown as { clearQueue: () => unknown }).clearQueue = () => ({ + steering: [{ text: "session steer" }], + followUp: [{ text: "session followup" }], + }); + const restored = new InputController(ctx).restoreQueuedMessagesToEditor(); + expect(restored).toBe(4); + expect(ctx.editor.getText()).toBe("session steer\n\ncompaction steer\n\nsession followup\n\ncompaction followup"); + }); +});