diff --git a/ui/src/pages/chat/chat-send-actions.ts b/ui/src/pages/chat/chat-send-actions.ts index 538c1840ce57..ac1292d861ee 100644 --- a/ui/src/pages/chat/chat-send-actions.ts +++ b/ui/src/pages/chat/chat-send-actions.ts @@ -44,7 +44,7 @@ import { QUEUED_MESSAGE_RETRY_CONFLICT_ERROR, QUEUED_MESSAGE_REORDER_CONFLICT_ERROR, } from "./queued-message-edit.ts"; -import { hasAbortableSessionRun } from "./run-lifecycle.ts"; +import { hasDirectSessionRun } from "./run-lifecycle.ts"; import { OFFLINE_QUEUE_STORAGE_ERROR, steerQueuedChatMessage as steerQueuedChatMessageLifecycle, @@ -240,7 +240,7 @@ export async function retryQueuedChatMessage(host: ChatHost, id: string) { setChatError(host, t("chat.sendErrors.steerRunNoLongerActive")); return; } - if (hasAbortableSessionRun(host)) { + if (hasDirectSessionRun(host)) { const retry = updateQueuedMessage(host, id, (entry) => ({ ...entry, sendAttempts: 0, diff --git a/ui/src/pages/chat/chat-send-delivery.ts b/ui/src/pages/chat/chat-send-delivery.ts index ea6a26377e78..b25f476cc4f3 100644 --- a/ui/src/pages/chat/chat-send-delivery.ts +++ b/ui/src/pages/chat/chat-send-delivery.ts @@ -61,7 +61,7 @@ import { formatConnectError } from "./connect-error.ts"; import { readChatSessionProjectionScope, reduceChatSessionProjection } from "./history-merge.ts"; import { resetChatInputHistoryNavigation } from "./input-history.ts"; import { controlUiNowMs, roundedControlUiDurationMs } from "./performance.ts"; -import { hasAbortableSessionRun, isChatBusy, reconcileChatRunLifecycle } from "./run-lifecycle.ts"; +import { hasDirectSessionRun, isChatBusy, reconcileChatRunLifecycle } from "./run-lifecycle.ts"; import { resetChatScroll, scheduleChatScroll } from "./scroll.ts"; import { formatTerminalChatSendAckError, @@ -614,7 +614,7 @@ export async function deliverChatQueueItem( if ( drainResult === undefined && routeVisible && - (isChatBusy(host) || hasAbortableSessionRun(host)) + (isChatBusy(host) || hasDirectSessionRun(host)) ) { const parked = finishChatDeliveryAdmission( host, diff --git a/ui/src/pages/chat/chat-send-queue-state.ts b/ui/src/pages/chat/chat-send-queue-state.ts index c7a07fb02af4..2efaf2c1928a 100644 --- a/ui/src/pages/chat/chat-send-queue-state.ts +++ b/ui/src/pages/chat/chat-send-queue-state.ts @@ -19,7 +19,7 @@ import { recordChatSendTiming, schedulePendingSendPaintTiming } from "./chat-sen import { getPendingChatPickerPatch } from "./chat-session.ts"; import { storedChatOutboxScopeKey, type StoredChatOutboxScope } from "./composer-persistence.ts"; import { controlUiNowMs } from "./performance.ts"; -import { hasAbortableSessionRun, isChatBusy } from "./run-lifecycle.ts"; +import { hasDirectSessionRun, isChatBusy } from "./run-lifecycle.ts"; import { scheduleChatScroll } from "./scroll.ts"; import { OFFLINE_QUEUE_STORAGE_ERROR, surfaceChatDeliveryFailure } from "./steer-lifecycle.ts"; @@ -192,7 +192,7 @@ export function finishChatDeliveryAdmission( } return "pending"; } - if (routeVisible(current.agentId) && (isChatBusy(host) || hasAbortableSessionRun(host))) { + if (routeVisible(current.agentId) && (isChatBusy(host) || hasDirectSessionRun(host))) { const parked = setState(host.connected && host.client ? "waiting-idle" : "waiting-reconnect"); if (!parked) { setChatError(host, OFFLINE_QUEUE_STORAGE_ERROR); diff --git a/ui/src/pages/chat/chat-send-submit.ts b/ui/src/pages/chat/chat-send-submit.ts index 0168ace7b464..959969ba8a97 100644 --- a/ui/src/pages/chat/chat-send-submit.ts +++ b/ui/src/pages/chat/chat-send-submit.ts @@ -61,6 +61,7 @@ import { activeQueuedMessageEdit, retireEditedQueuedMessageSource } from "./queu import { handleAbortChat, hasAbortableSessionRun, + hasDirectSessionRun, isChatBusy, isChatStopCommand, } from "./run-lifecycle.ts"; @@ -584,7 +585,7 @@ export async function handleSendChat( pending?.sendState === "waiting-idle" && host.sessionKey === submittedSessionKey && visibleSessionMatches(host, submittedSessionKey, pending.agentId) && - (isChatBusy(host) || hasAbortableSessionRun(host)); + (isChatBusy(host) || hasDirectSessionRun(host)); if (pendingBusySend) { recordChatSendTiming(host, pending, "queued-busy", submittedAtMs); // Only an explicit browser override replaces inherited Gateway policy. @@ -596,7 +597,7 @@ export async function handleSendChat( !skillWorkshopRevision && followUpMode !== "queue" && host.connected && - hasAbortableSessionRun(host) + hasDirectSessionRun(host) ) { void sendQueuedChatMessageWithQueueModeLifecycle( host, diff --git a/ui/src/pages/chat/chat-send.test.ts b/ui/src/pages/chat/chat-send.test.ts index 97f10cc9b8f9..5c12a2d82df8 100644 --- a/ui/src/pages/chat/chat-send.test.ts +++ b/ui/src/pages/chat/chat-send.test.ts @@ -3873,6 +3873,34 @@ describe("handleSendChat", () => { ); }); + it("sends normally when only a descendant run is active", async () => { + const host = makeChatHost({ + requestHandlers: { + "chat.send": { status: "started", runId: "new-parent-run" }, + }, + chatMessage: "start another parent turn", + chatRunId: null, + sessionKey: "agent:main:main", + sessionsResult: createSessionsResult([ + row("agent:main:main", { + hasActiveRun: false, + hasActiveSubagentRun: true, + status: "done", + }), + ]), + settings: { chatFollowUpMode: "steer" }, + }); + + await handleSendChat(host); + + await waitForFast(() => expect(host.request).toHaveBeenCalled()); + const payload = findRequestPayload(host.request, "chat.send", "chat send payload"); + expect(payload.message).toBe("start another parent turn"); + expect(payload).not.toHaveProperty("queueMode"); + expect(payload).not.toHaveProperty("expectedRunId"); + expect(host.chatError).toBeNull(); + }); + it("keeps a steered message visible when only the session row reports an active run", async () => { let wireRunId: unknown; diff --git a/ui/src/pages/chat/run-lifecycle.test.ts b/ui/src/pages/chat/run-lifecycle.test.ts index 135253f473c3..a6b3c6cc0089 100644 --- a/ui/src/pages/chat/run-lifecycle.test.ts +++ b/ui/src/pages/chat/run-lifecycle.test.ts @@ -9,6 +9,7 @@ import { CHAT_RUN_STATUS_TOAST_DURATION_MS, handleAbortChat, hasAbortableSessionRun, + hasDirectSessionRun, reconcileChatRunFromCurrentSessionRow, reconcileChatRunFromSessionRow, reconcileChatRunLifecycle, @@ -80,6 +81,7 @@ describe("handleAbortChat", () => { ]), }); + expect(hasDirectSessionRun(host)).toBe(false); expect(hasAbortableSessionRun(host)).toBe(true); await handleAbortChat(host); diff --git a/ui/src/pages/chat/run-lifecycle.ts b/ui/src/pages/chat/run-lifecycle.ts index 650d8e6393c2..866e86c6346b 100644 --- a/ui/src/pages/chat/run-lifecycle.ts +++ b/ui/src/pages/chat/run-lifecycle.ts @@ -140,23 +140,35 @@ export function isChatBusy(host: { chatSending?: boolean; chatRunId?: string | n return Boolean(host.chatSending || host.chatRunId); } -export function hasAbortableSessionRun(host: { +type SessionRunHost = { chatRunId?: string | null; sessionKey: string; sessionsResult?: SessionsListResult | null; -}): boolean { - if (host.chatRunId) { - return true; - } +}; + +export function hasDirectSessionRun(host: SessionRunHost): boolean { return Boolean( + host.chatRunId || host.sessionsResult?.sessions.some( (session) => - areUiSessionKeysEquivalent(session.key, host.sessionKey) && - (isSessionRunActive(session) || session.hasActiveSubagentRun === true), + areUiSessionKeysEquivalent(session.key, host.sessionKey) && isSessionRunActive(session), ), ); } +export function hasAbortableSessionRun(host: SessionRunHost): boolean { + return ( + hasDirectSessionRun(host) || + Boolean( + host.sessionsResult?.sessions.some( + (session) => + areUiSessionKeysEquivalent(session.key, host.sessionKey) && + session.hasActiveSubagentRun === true, + ), + ) + ); +} + export function isChatStopCommand(text: string) { return CHAT_STOP_COMMANDS.has(normalizeLowercaseStringOrEmpty(text.trim())); } diff --git a/ui/src/pages/chat/steer-lifecycle.ts b/ui/src/pages/chat/steer-lifecycle.ts index c210e8aef5d4..a2213c453e75 100644 --- a/ui/src/pages/chat/steer-lifecycle.ts +++ b/ui/src/pages/chat/steer-lifecycle.ts @@ -41,7 +41,7 @@ import { isQueuedMessageBeingEdited, QUEUED_MESSAGE_STEER_CONFLICT_ERROR, } from "./queued-message-edit.ts"; -import { hasAbortableSessionRun } from "./run-lifecycle.ts"; +import { hasDirectSessionRun } from "./run-lifecycle.ts"; import { scheduleChatScroll, type ChatScrollHost } from "./scroll.ts"; import { appendChatMessageToCache, readChatMessagesFromCache } from "./session-message-cache.ts"; import { @@ -368,7 +368,7 @@ export async function sendQueuedChatMessageWithQueueMode( queueMode: QueueMode | undefined, dependencies: SteerSendDependencies, ): Promise { - if (!host.connected || !hasAbortableSessionRun(host)) { + if (!host.connected || !hasDirectSessionRun(host)) { return; } const isSteer = queueMode === "steer";