Skip to content
Merged
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
27 changes: 15 additions & 12 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -974,11 +974,14 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
[lockedProvider, providerModelsByProvider, selectableProviders],
);
const phase = derivePhase(activeThread?.session ?? null);
const isTurnActive =
activeThread?.session?.activeTurnId !== undefined &&
activeThread?.session?.activeTurnId !== null;
const isSendBusy = sendPhase !== "idle";
const isPreparingWorktree = sendPhase === "preparing-worktree";
const isTransportReady = transportState === "open";
const isRemoteActionBlocked = !isTransportReady;
const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint;
const isWorking = isTurnActive || isSendBusy || isConnecting || isRevertingCheckpoint;
const nowIso = new Date(nowTick).toISOString();
const activeWorkStartedAt = deriveActiveWorkStartedAt(
activeLatestTurn,
Expand Down Expand Up @@ -2530,10 +2533,10 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
scheduleStickToBottom();
}, [messageCount, scheduleStickToBottom]);
useEffect(() => {
if (phase !== "running") return;
if (!isTurnActive) return;
if (!shouldAutoScrollRef.current) return;
scheduleStickToBottom();
}, [phase, scheduleStickToBottom, timelineEntries]);
}, [isTurnActive, scheduleStickToBottom, timelineEntries]);

// Aggressively scroll to bottom after the user submits a new message.
// The virtualizer may not have settled by the time the first scroll fires,
Expand Down Expand Up @@ -2778,14 +2781,14 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
: "local";

useEffect(() => {
if (phase !== "running") return;
if (!isTurnActive) return;
const timer = window.setInterval(() => {
setNowTick(Date.now());
}, 1000);
return () => {
window.clearInterval(timer);
};
}, [phase]);
}, [isTurnActive]);

const beginSendPhase = useCallback((nextPhase: Exclude<SendPhase, "idle">) => {
setSendStartedAt((current) => current ?? new Date().toISOString());
Expand All @@ -2802,7 +2805,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
return;
}
if (
phase === "running" ||
isTurnActive ||
activePendingApproval !== null ||
activePendingUserInput !== null ||
activeThread?.error
Expand All @@ -2813,7 +2816,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
activePendingApproval,
activePendingUserInput,
activeThread?.error,
phase,
isTurnActive,
resetSendPhase,
sendPhase,
]);
Expand Down Expand Up @@ -3171,7 +3174,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
const api = readNativeApi();
if (!api || !activeThread || isRevertingCheckpoint) return;

if (phase === "running" || isSendBusy || isConnecting) {
if (isTurnActive || isSendBusy || isConnecting) {
setThreadError(activeThread.id, "Interrupt the current turn before reverting checkpoints.");
return;
}
Expand Down Expand Up @@ -3204,7 +3207,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
}
setIsRevertingCheckpoint(false);
},
[activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, phase, setThreadError],
[activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, isTurnActive, setThreadError],
);

const readLiveComposerDraftSnapshot = useCallback(() => {
Expand Down Expand Up @@ -3448,7 +3451,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
}

// ── Queue message if a turn is already running ────────────────────
if (phase === "running") {
if (isTurnActive) {
const composerAttachmentsSnapshot = [...composerAttachmentsForSend];
const hiddenProviderInput = buildHiddenProviderInput({
prompt: promptForSend,
Expand Down Expand Up @@ -5548,7 +5551,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
Preparing worktree...
</span>
) : null}
{queuedMessages.length > 0 && phase === "running" ? (
{queuedMessages.length > 0 && isTurnActive ? (
<button
type="button"
className="flex items-center gap-1 text-muted-foreground/60 text-xs transition-colors hover:text-destructive"
Expand Down Expand Up @@ -5605,7 +5608,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
: "Next question"}
</Button>
</div>
) : phase === "running" ? (
) : isTurnActive ? (
<div className="flex items-center gap-1.5">
<button
type="button"
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/session-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,15 @@ describe("isLatestTurnSettled", () => {
).toBe(false);
});

it("returns true when the session is running but no turn is active", () => {
expect(
isLatestTurnSettled(latestTurn, {
orchestrationStatus: "running",
activeTurnId: undefined,
}),
).toBe(true);
});

it("returns true once the session is no longer running that turn", () => {
expect(
isLatestTurnSettled(latestTurn, {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/session-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export function isLatestTurnSettled(
if (!latestTurn?.startedAt) return false;
if (!latestTurn.completedAt) return false;
if (!session) return true;
if (session.orchestrationStatus === "running") return false;
if (session.activeTurnId !== undefined && session.activeTurnId !== null) return false;
return true;
}

Expand Down
Loading