fix(coding-agent-turn-interrupt/queue-ux): resolved steering abort state

- Replaced queued-message interrupt flow with session abort calls on empty submit and escape.
- Removed interrupting state and notifyInterrupting teardown paths from abort handling.
- Updated AgentSession queue operations to use shared steering and follow-up queue views.
- Propagated isAborting through session state and collab payloads to suppress late updates.
This commit is contained in:
can1357
2026-06-13 17:19:57 +02:00
parent 3def4a6979
commit 705750453d
23 changed files with 440 additions and 1304 deletions
@@ -1,9 +1,8 @@
/**
* Contract: a guest prompt that arrives while the host agent is streaming is
* steered AND becomes visible as a queued message — the host registers the
* pending-display twin (so `queuedMessageCount` covers it, feeding the web
* composer's "queued ×N" badge and the TUI pending bar) and tags the custom
* message so the entry is dequeued when the agent consumes it.
* steered AND becomes visible as a queued message. The host sends the guest text
* as `queueChipText` on the queued custom message; the session derives
* `queuedMessageCount` from the agent-core queue for host and guest UI state.
*/
import { afterEach, describe, expect, it } from "bun:test";
import { importRoomKey } from "@oh-my-pi/pi-coding-agent/collab/crypto";
@@ -76,20 +75,19 @@ function startTestRelay(): { url: string; stop(): void } {
}
interface CapturedPrompt {
details?: { from?: string; __pendingDisplayTag?: string };
details?: { from?: string };
options?: { streamingBehavior?: "steer"; queueChipText?: string };
}
interface StreamingHostHarness {
ctx: InteractiveModeContext;
prompts: CapturedPrompt[];
enqueued: { text: string; mode: string; tag: string }[];
nextPrompt(): Promise<CapturedPrompt>;
}
/** Context double for a host whose agent is mid-turn (isStreaming === true). */
function makeStreamingHostContext(): StreamingHostHarness {
const prompts: CapturedPrompt[] = [];
const enqueued: { text: string; mode: string; tag: string }[] = [];
const promptWaiters: ((prompt: CapturedPrompt) => void)[] = [];
const ctx = {
settings: { get: () => "" },
@@ -105,20 +103,16 @@ function makeStreamingHostContext(): StreamingHostHarness {
session: {
isStreaming: true,
get queuedMessageCount(): number {
return enqueued.length;
return prompts.filter(prompt => prompt.options?.queueChipText).length;
},
isAborting: false,
sessionName: "test",
model: undefined,
thinkingLevel: undefined,
subscribe: () => () => {},
emitNotice: () => {},
enqueueCustomMessageDisplay: (text: string, mode: string) => {
const tag = `tag-${enqueued.length + 1}`;
enqueued.push({ text, mode, tag });
return tag;
},
promptCustomMessage: (message: CapturedPrompt) => {
const captured: CapturedPrompt = { details: message.details };
promptCustomMessage: (message: CapturedPrompt, options?: CapturedPrompt["options"]) => {
const captured: CapturedPrompt = { details: message.details, options };
prompts.push(captured);
for (const waiter of promptWaiters.splice(0)) waiter(captured);
return Promise.resolve();
@@ -140,7 +134,7 @@ function makeStreamingHostContext(): StreamingHostHarness {
promptWaiters.push(resolve);
return promise;
};
return { ctx, prompts, enqueued, nextPrompt };
return { ctx, prompts, nextPrompt };
}
interface TestGuest {
@@ -197,10 +191,8 @@ describe("collab mid-turn guest prompts", () => {
guest.socket.send({ t: "prompt", text: "steer the host" });
const prompt = await prompted;
// Display twin registered before dispatch, tag forwarded in details so
// the session dequeues the entry when the agent consumes the message.
expect(harness.enqueued).toEqual([{ text: "steer the host", mode: "steer", tag: "tag-1" }]);
expect(prompt.details).toEqual({ from: "writer", __pendingDisplayTag: "tag-1" });
expect(prompt.details).toEqual({ from: "writer" });
expect(prompt.options).toEqual({ streamingBehavior: "steer", queueChipText: "steer the host" });
// The queued steer must reach guests through state.queuedMessageCount —
// that field drives the web composer's "queued ×N" badge.