Add active-turn steering (#3394)

* feat(agents): add active-turn steering

Let users choose whether an immediate prompt interrupts or joins the active turn. The daemon owns exact-turn admission, provider delivery, safe fallback, and canonical timeline identity while queueing remains client-side.

* fix(agents): stabilize steering history and replay QA

* fix(agents): preserve steering timeline order

* fix(agents): reconcile resumed provider history

* fix(agents): serialize steering with cancellation

* test(agents): prove Claude multi-tool steering

* test(agents): force explicit history hydration

* fix(agents): flush output before steer admission
This commit is contained in:
Mohamed Boudra
2026-08-16 13:58:40 +02:00
committed by GitHub
parent 16120ebff1
commit 42245d139a
69 changed files with 5203 additions and 269 deletions
@@ -35,6 +35,14 @@ function makeDeps(
}
describe("loadAppSettingsFromStorage", () => {
it("preserves a persisted steer send behavior", async () => {
const deps = makeDeps({
storage: createInMemoryKeyValueStorage({
"@paseo:app-settings": JSON.stringify({ sendBehavior: "steer" }),
}),
});
expect((await loadAppSettingsFromStorage(deps)).sendBehavior).toBe("steer");
});
it("defaults theme to auto when storage is empty", async () => {
const deps = makeDeps();
@@ -1,4 +1,5 @@
import { isSyntaxThemeId, type SyntaxThemeId } from "@getpaseo/highlight";
import type { ActiveTurnBehavior } from "@getpaseo/protocol/messages";
import type { QueryClient } from "@tanstack/react-query";
import type { DesktopSettings } from "@/desktop/settings/desktop-settings";
import { parseAppLanguage, type AppLanguage } from "@/i18n/locales";
@@ -21,7 +22,7 @@ export const APP_SETTINGS_KEY = "@paseo:app-settings";
export const APP_SETTINGS_QUERY_KEY = ["app-settings"];
const LEGACY_SETTINGS_KEY = "@paseo:settings";
export type SendBehavior = "interrupt" | "queue";
export type SendBehavior = ActiveTurnBehavior | "queue";
export type ReleaseChannel = "stable" | "beta";
export type ServiceUrlBehavior = "ask" | "in-app" | "external";
export type WorkspaceTitleSource = "title" | "branch";
@@ -92,7 +93,7 @@ const StoredAppSettingsSchema = z.strictObject({
language: z
.enum(["system", "ar", "en", "es", "fr", "ja", "ko", "pt-BR", "ru", "zh-CN"])
.optional(),
sendBehavior: z.enum(["interrupt", "queue"]).optional(),
sendBehavior: z.enum(["interrupt", "steer", "queue"]).optional(),
serviceUrlBehavior: z.enum(["ask", "in-app", "external"]).optional(),
terminalScrollbackLines: z.union([z.number(), z.string()]).optional(),
useLegacyTerminalRenderer: z.boolean().optional(),
@@ -296,7 +297,11 @@ function pickEnumAppSettings(stored: StoredAppSettings): Partial<AppSettings> {
if (typeof stored.theme === "string" && VALID_THEMES.has(stored.theme)) {
result.theme = stored.theme;
}
if (stored.sendBehavior === "interrupt" || stored.sendBehavior === "queue") {
if (
stored.sendBehavior === "interrupt" ||
stored.sendBehavior === "steer" ||
stored.sendBehavior === "queue"
) {
result.sendBehavior = stored.sendBehavior;
}
if (