Skip to content

Commit 6fbb060

Browse files
authored
Treat active turn as the source of working state (#453)
- Use activeTurnId instead of orchestration status for UI busy/scroll gating - Update settled-turn logic and tests for sessions that are running without an active turn
1 parent 0c385fd commit 6fbb060

3 files changed

Lines changed: 25 additions & 13 deletions

File tree

apps/web/src/components/ChatView.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -973,11 +973,14 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
973973
[lockedProvider, providerModelsByProvider, selectableProviders],
974974
);
975975
const phase = derivePhase(activeThread?.session ?? null);
976+
const isTurnActive =
977+
activeThread?.session?.activeTurnId !== undefined &&
978+
activeThread?.session?.activeTurnId !== null;
976979
const isSendBusy = sendPhase !== "idle";
977980
const isPreparingWorktree = sendPhase === "preparing-worktree";
978981
const isTransportReady = transportState === "open";
979982
const isRemoteActionBlocked = !isTransportReady;
980-
const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint;
983+
const isWorking = isTurnActive || isSendBusy || isConnecting || isRevertingCheckpoint;
981984
const nowIso = new Date(nowTick).toISOString();
982985
const activeWorkStartedAt = deriveActiveWorkStartedAt(
983986
activeLatestTurn,
@@ -2529,10 +2532,10 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
25292532
scheduleStickToBottom();
25302533
}, [messageCount, scheduleStickToBottom]);
25312534
useEffect(() => {
2532-
if (phase !== "running") return;
2535+
if (!isTurnActive) return;
25332536
if (!shouldAutoScrollRef.current) return;
25342537
scheduleStickToBottom();
2535-
}, [phase, scheduleStickToBottom, timelineEntries]);
2538+
}, [isTurnActive, scheduleStickToBottom, timelineEntries]);
25362539

25372540
// Aggressively scroll to bottom after the user submits a new message.
25382541
// The virtualizer may not have settled by the time the first scroll fires,
@@ -2777,14 +2780,14 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
27772780
: "local";
27782781

27792782
useEffect(() => {
2780-
if (phase !== "running") return;
2783+
if (!isTurnActive) return;
27812784
const timer = window.setInterval(() => {
27822785
setNowTick(Date.now());
27832786
}, 1000);
27842787
return () => {
27852788
window.clearInterval(timer);
27862789
};
2787-
}, [phase]);
2790+
}, [isTurnActive]);
27882791

27892792
const beginSendPhase = useCallback((nextPhase: Exclude<SendPhase, "idle">) => {
27902793
setSendStartedAt((current) => current ?? new Date().toISOString());
@@ -2801,7 +2804,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
28012804
return;
28022805
}
28032806
if (
2804-
phase === "running" ||
2807+
isTurnActive ||
28052808
activePendingApproval !== null ||
28062809
activePendingUserInput !== null ||
28072810
activeThread?.error
@@ -2812,7 +2815,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
28122815
activePendingApproval,
28132816
activePendingUserInput,
28142817
activeThread?.error,
2815-
phase,
2818+
isTurnActive,
28162819
resetSendPhase,
28172820
sendPhase,
28182821
]);
@@ -3170,7 +3173,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
31703173
const api = readNativeApi();
31713174
if (!api || !activeThread || isRevertingCheckpoint) return;
31723175

3173-
if (phase === "running" || isSendBusy || isConnecting) {
3176+
if (isTurnActive || isSendBusy || isConnecting) {
31743177
setThreadError(activeThread.id, "Interrupt the current turn before reverting checkpoints.");
31753178
return;
31763179
}
@@ -3203,7 +3206,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
32033206
}
32043207
setIsRevertingCheckpoint(false);
32053208
},
3206-
[activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, phase, setThreadError],
3209+
[activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, isTurnActive, setThreadError],
32073210
);
32083211

32093212
const readLiveComposerDraftSnapshot = useCallback(() => {
@@ -3447,7 +3450,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
34473450
}
34483451

34493452
// ── Queue message if a turn is already running ────────────────────
3450-
if (phase === "running") {
3453+
if (isTurnActive) {
34513454
const composerAttachmentsSnapshot = [...composerAttachmentsForSend];
34523455
const hiddenProviderInput = buildHiddenProviderInput({
34533456
prompt: promptForSend,
@@ -5547,7 +5550,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
55475550
Preparing worktree...
55485551
</span>
55495552
) : null}
5550-
{queuedMessages.length > 0 && phase === "running" ? (
5553+
{queuedMessages.length > 0 && isTurnActive ? (
55515554
<button
55525555
type="button"
55535556
className="flex items-center gap-1 text-muted-foreground/60 text-xs transition-colors hover:text-destructive"
@@ -5604,7 +5607,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
56045607
: "Next question"}
56055608
</Button>
56065609
</div>
5607-
) : phase === "running" ? (
5610+
) : isTurnActive ? (
56085611
<div className="flex items-center gap-1.5">
56095612
<button
56105613
type="button"

apps/web/src/session-logic.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,15 @@ describe("isLatestTurnSettled", () => {
10571057
).toBe(false);
10581058
});
10591059

1060+
it("returns true when the session is running but no turn is active", () => {
1061+
expect(
1062+
isLatestTurnSettled(latestTurn, {
1063+
orchestrationStatus: "running",
1064+
activeTurnId: undefined,
1065+
}),
1066+
).toBe(true);
1067+
});
1068+
10601069
it("returns true once the session is no longer running that turn", () => {
10611070
expect(
10621071
isLatestTurnSettled(latestTurn, {

apps/web/src/session-logic.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export function isLatestTurnSettled(
146146
if (!latestTurn?.startedAt) return false;
147147
if (!latestTurn.completedAt) return false;
148148
if (!session) return true;
149-
if (session.orchestrationStatus === "running") return false;
149+
if (session.activeTurnId !== undefined && session.activeTurnId !== null) return false;
150150
return true;
151151
}
152152

0 commit comments

Comments
 (0)