Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions packages/ai/test/pi-native-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,39 @@ function stalledBody(bytes: Uint8Array[] = []): ReadableStream<Uint8Array> {
}

function delayedBody(chunks: Array<{ atMs: number; bytes: Uint8Array }>): ReadableStream<Uint8Array> {
let closed = false;
const timers: Timer[] = [];
const clearTimers = () => {
closed = true;
for (const timer of timers) clearTimeout(timer);
timers.length = 0;
};
return new ReadableStream<Uint8Array>({
start(controller) {
const enqueue = (bytes: Uint8Array) => {
if (!closed) controller.enqueue(bytes);
};
for (const chunk of chunks) {
setTimeout(() => controller.enqueue(chunk.bytes), chunk.atMs);
if (chunk.atMs <= 0) {
enqueue(chunk.bytes);
} else {
timers.push(setTimeout(() => enqueue(chunk.bytes), chunk.atMs));
}
}
setTimeout(() => controller.close(), Math.max(...chunks.map(chunk => chunk.atMs)) + 1);
timers.push(
setTimeout(
() => {
if (!closed) {
clearTimers();
controller.close();
}
},
Math.max(...chunks.map(chunk => chunk.atMs)) + 1,
),
);
},
cancel() {
clearTimers();
},
});
}
Expand Down
3 changes: 3 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased]

### Fixed

- Fixed rapid queued steer/follow-up image submissions racing into split or dropped pending entries by serializing queue mutations; added opt-in `coalescing` queue mode to merge rapid consecutive queued user entries while preserving attachments, hidden magic-keyword companions, restore behavior, delivery, and `[Image #N]` marker numbering.
## [16.3.11] - 2026-07-06

### Changed
Expand Down
6 changes: 6 additions & 0 deletions packages/coding-agent/src/config/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface AppKeybindings {
"app.message.followUp": true;
"app.retry": true;
"app.message.dequeue": true;
"app.message.expandQueue": true;
"app.clipboard.pasteImage": true;
"app.clipboard.pasteTextRaw": true;
"app.clipboard.copyLine": true;
Expand Down Expand Up @@ -142,6 +143,10 @@ export const KEYBINDINGS = {
defaultKeys: "alt+up",
description: "Dequeue message",
},
"app.message.expandQueue": {
defaultKeys: "alt+o",
description: "Expand/collapse queued message preview",
},
"app.clipboard.pasteImage": {
defaultKeys: getDefaultPasteImageKeys(),
description: "Paste image or text from clipboard",
Expand Down Expand Up @@ -247,6 +252,7 @@ const KEYBINDING_NAME_MIGRATIONS = {
followUp: "app.message.followUp",
retry: "app.retry",
dequeue: "app.message.dequeue",
expandQueue: "app.message.expandQueue",
pasteImage: "app.clipboard.pasteImage",
pasteTextRaw: "app.clipboard.pasteTextRaw",
copyLine: "app.clipboard.copyLine",
Expand Down
25 changes: 21 additions & 4 deletions packages/coding-agent/src/config/settings-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1407,25 +1407,25 @@ export const SETTINGS_SCHEMA = {
// Conversation flow
steeringMode: {
type: "enum",
values: ["all", "one-at-a-time"] as const,
values: ["all", "one-at-a-time", "coalescing"] as const,
default: "one-at-a-time",
ui: {
tab: "interaction",
group: "Input",
label: "Steering Mode",
description: "How to process queued messages while agent is working",
description: "How to process queued steering messages while the agent is working",
},
},

followUpMode: {
type: "enum",
values: ["all", "one-at-a-time"] as const,
values: ["all", "one-at-a-time", "coalescing"] as const,
default: "one-at-a-time",
ui: {
tab: "interaction",
group: "Input",
label: "Follow-Up Mode",
description: "How to drain follow-up messages after a turn completes",
description: "How to drain queued follow-up messages after a turn completes",
},
},

Expand All @@ -1440,6 +1440,23 @@ export const SETTINGS_SCHEMA = {
description: "When steering messages interrupt tool execution",
},
},
pendingQueueCollapseLines: {
type: "number",
default: 5,
ui: {
tab: "interaction",
group: "Input",
label: "Queued Message Preview Lines",
description:
"How many leading lines of each queued steer/follow-up message the pending bar shows before collapsing the rest to `(+N)`. Alt+O expands every entry to its full text and toggles back to this collapsed preview.",
options: [
{ value: "1", label: "1 line" },
{ value: "3", label: "3 lines" },
{ value: "5", label: "5 lines" },
{ value: "10", label: "10 lines" },
],
},
},

"loop.mode": {
type: "enum",
Expand Down
16 changes: 16 additions & 0 deletions packages/coding-agent/src/modes/components/custom-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type ConfigurableEditorAction = Extract<
| "app.editor.external"
| "app.history.search"
| "app.message.dequeue"
| "app.message.expandQueue"
| "app.retry"
| "app.clipboard.pasteImage"
| "app.clipboard.pasteTextRaw"
Expand All @@ -47,6 +48,7 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
"app.editor.external": ["ctrl+g"],
"app.history.search": ["ctrl+r"],
"app.message.dequeue": ["alt+up"],
"app.message.expandQueue": ["alt+o"],
"app.retry": ["alt+r"],
"app.clipboard.pasteImage": ["ctrl+v"],
"app.clipboard.pasteTextRaw": ["ctrl+shift+v", "alt+shift+v"],
Expand Down Expand Up @@ -414,12 +416,16 @@ export class CustomEditor extends Editor {
onPasteTextRaw?: () => void;
/** Called when the configured dequeue shortcut is pressed. */
onDequeue?: () => void;
/** Called when the configured expand-queue shortcut is pressed (toggle queued-message preview). */
onExpandQueue?: () => void;
/** Called when the configured retry shortcut is pressed. */
onRetry?: () => void;
/** Called when Caps Lock is pressed. */
onCapsLock?: () => void;
/** Called when left-arrow is pressed while the editor is empty (cursor necessarily at start). */
onLeftAtStart?: () => void;
/** Called when Up is pressed on an empty editor. Return true to consume it before history navigation. */
onUpWhenEmpty?: () => boolean;

/** Fired when a sustained space-bar hold is recognized — the push-to-talk STT start. The
* optimistically-typed spaces have already been deleted by the time this runs. */
Expand Down Expand Up @@ -687,6 +693,10 @@ export class CustomEditor extends Editor {
return;
}

if (canonical === "up" && this.onUpWhenEmpty && this.getText().trim() === "" && this.onUpWhenEmpty()) {
return;
}

// Space-hold push-to-talk: a sustained space bar starts/stops STT instead of typing spaces.
if (this.#handleSpaceHold(data, canonical)) return;

Expand Down Expand Up @@ -801,6 +811,12 @@ export class CustomEditor extends Editor {
return;
}

// Intercept configured expand-queue shortcut (toggle queued-message preview)
if (this.#matchesAction(canonical, "app.message.expandQueue") && this.onExpandQueue) {
this.onExpandQueue();
return;
}

// Intercept configured copy-prompt shortcut
if (this.#matchesAction(canonical, "app.clipboard.copyPrompt") && this.onCopyPrompt) {
this.onCopyPrompt();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Container, type SelectItem, SelectList, type SgrMouseEvent } from "@oh-my-pi/pi-tui";
import { getSelectListTheme } from "../../modes/theme/theme";
import type { QueueMode } from "../../session/agent-session";
import { DynamicBorder } from "./dynamic-border";
import { routeSelectListMouseWithTopBorder } from "./select-list-mouse-routing";

Expand All @@ -9,18 +10,19 @@ import { routeSelectListMouseWithTopBorder } from "./select-list-mouse-routing";
export class QueueModeSelectorComponent extends Container {
#selectList: SelectList;

constructor(
currentMode: "all" | "one-at-a-time",
onSelect: (mode: "all" | "one-at-a-time") => void,
onCancel: () => void,
) {
constructor(currentMode: QueueMode, onSelect: (mode: QueueMode) => void, onCancel: () => void) {
super();

const queueModes: SelectItem[] = [
{
value: "one-at-a-time",
label: "one-at-a-time",
description: "Process queued messages one by one (recommended)",
description: "Process queued messages one by one (default)",
},
{
value: "coalescing",
label: "coalescing",
description: "Merge rapid consecutive queued messages into one pending entry",
},
{ value: "all", label: "all", description: "Process all queued messages at once" },
];
Expand All @@ -38,7 +40,7 @@ export class QueueModeSelectorComponent extends Container {
}

this.#selectList.onSelect = item => {
onSelect(item.value as "all" | "one-at-a-time");
onSelect(item.value as QueueMode);
};

this.#selectList.onCancel = () => {
Expand Down
Loading
Loading