Skip to content
Closed
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
41 changes: 22 additions & 19 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,6 @@ import {
buildLocalDraftThread,
buildLoadingThreadFromShell,
buildRunningThreadTurnInterruptInput,
buildThreadTurnInterruptInput,
collectUserMessageBlobPreviewUrls,
createLocalDispatchSnapshot,
deriveComposerSendState,
Expand Down Expand Up @@ -1445,6 +1444,9 @@ export default function ChatView(props: ChatViewProps) {
const interruptThreadTurn = useAtomCommand(threadEnvironment.interruptTurn, {
reportFailure: false,
});
const stopThreadSession = useAtomCommand(threadEnvironment.stopSession, {
reportFailure: false,
});
const respondToThreadApproval = useAtomCommand(threadEnvironment.respondToApproval, {
reportFailure: false,
});
Expand Down Expand Up @@ -5719,45 +5721,46 @@ export default function ChatView(props: ChatViewProps) {
]);
// Background work (subagent fleets, workflow runs, watch loops) can outlive
// the turn; once it settles, the composer stop button is gone, so this
// banner is the only visible stop affordance. Stop routes through the
// stop-everything interrupt: it kills every live background task before
// interrupting, and works by session, so no active turn is needed.
// banner is the only visible stop affordance. Stop ends the provider session,
// which tears down its background work and emits the liveness update.
const activeBackgroundLiveness =
!isWorking && activeThread ? (activeThreadShell?.backgroundLiveness ?? null) : null;
const [isStoppingBackgroundWork, setIsStoppingBackgroundWork] = useState(false);
!isWorking && activeThread && activeThreadShell?.session?.status !== "stopped"
? (activeThreadShell?.backgroundLiveness ?? null)
: null;
const [stoppingBackgroundWorkThreadId, setStoppingBackgroundWorkThreadId] =
useState<ThreadId | null>(null);
const isStoppingBackgroundWork = activeThread?.id === stoppingBackgroundWorkThreadId;
useEffect(() => {
// "Stopping..." holds until the liveness clears; the interrupt command
// returning only means the request was accepted.
// The provider session stop is the hard boundary. Keep "Stopping..."
// visible until the existing liveness signal reports that it has cleared.
if (activeBackgroundLiveness === null) {
setIsStoppingBackgroundWork(false);
setStoppingBackgroundWorkThreadId(null);
}
}, [activeBackgroundLiveness]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
useEffect(() => {
// Per-thread state: switching threads while A's stop is pending must not
// disable B's Stop button (review finding).
setIsStoppingBackgroundWork(false);
setStoppingBackgroundWorkThreadId(null);
}, [activeThreadId]);
const handleStopBackgroundWork = useCallback(async () => {
if (!activeThread) return;
setIsStoppingBackgroundWork(true);
const result = await interruptThreadTurn({
const threadId = activeThread.id;
setStoppingBackgroundWorkThreadId(threadId);
const result = await stopThreadSession({
environmentId,
input: buildThreadTurnInterruptInput(activeThread),
input: { threadId },
});
if (result._tag === "Failure") {
// Every failure clears the pending state — an interrupted command
// never reached the server, so liveness would hold "Stopping..."
// forever. Only real failures toast.
setIsStoppingBackgroundWork(false);
setStoppingBackgroundWorkThreadId((current) => (current === threadId ? null : current));
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
setThreadError(
activeThread.id,
threadId,
error instanceof Error ? error.message : "Failed to stop background work.",
);
}
}
}, [activeThread, environmentId, interruptThreadTurn, setThreadError]);
}, [activeThread, environmentId, setThreadError, stopThreadSession]);
const backgroundLivenessBannerItem = useMemo<ComposerBannerStackItem | null>(() => {
if (activeBackgroundLiveness === null || !activeThread) {
return null;
Expand Down
Loading