authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-02 20:19:58-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-02 20:19:58-07:00
loge8f5b4ad5b28b63563fe3812998077ffb343aec8
tree25c901227bcb9f5bcc03db1a9f967c131cade1ed
parent752f96e9ae72be9d56686902454eb7a3c7d8686a
signature Signed by PGP key B5690EEEBB952194

[codex] Revert stale send spinner fix (#1704)

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 16 insertions(+), 118 deletions(-)

apps/web/src/components/ChatView.tsx+10-15
...@@ -52,7 +52,6 @@ import {...@@ -52,7 +52,6 @@ import {
52 deriveWorkLogEntries,52 deriveWorkLogEntries,
53 hasActionableProposedPlan,53 hasActionableProposedPlan,
54 hasToolActivityForTurn,54 hasToolActivityForTurn,
55 isSessionActivelyRunningTurn,
56 isLatestTurnSettled,55 isLatestTurnSettled,
57 formatElapsed,56 formatElapsed,
58} from "../session-logic";57} from "../session-logic";
...@@ -1121,11 +1120,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1121,11 +1120,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
1121 activePendingUserInput: activePendingUserInput?.requestId ?? null,1120 activePendingUserInput: activePendingUserInput?.requestId ?? null,
1122 threadError: activeThread?.error,1121 threadError: activeThread?.error,
1123 });1122 });
1124 const isTurnRunning = isSessionActivelyRunningTurn(1123 const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint;
1125 activeLatestTurn,
1126 activeThread?.session ?? null,
1127 );
1128 const isWorking = isTurnRunning || isSendBusy || isConnecting || isRevertingCheckpoint;
1129 const nowIso = new Date(nowTick).toISOString();1124 const nowIso = new Date(nowTick).toISOString();
1130 const activeWorkStartedAt = deriveActiveWorkStartedAt(1125 const activeWorkStartedAt = deriveActiveWorkStartedAt(
1131 activeLatestTurn,1126 activeLatestTurn,
...@@ -1142,7 +1137,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1142,7 +1137,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
1142 if (activePendingProgress) {1137 if (activePendingProgress) {
1143 return `pending:${activePendingProgress.questionIndex}:${activePendingProgress.isLastQuestion}:${activePendingIsResponding}`;1138 return `pending:${activePendingProgress.questionIndex}:${activePendingProgress.isLastQuestion}:${activePendingIsResponding}`;
1144 }1139 }
1145 if (isTurnRunning) {1140 if (phase === "running") {
1146 return "running";1141 return "running";
1147 }1142 }
1148 if (showPlanFollowUpPrompt) {1143 if (showPlanFollowUpPrompt) {
...@@ -1156,7 +1151,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1156,7 +1151,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
1156 isConnecting,1151 isConnecting,
1157 isPreparingWorktree,1152 isPreparingWorktree,
1158 isSendBusy,1153 isSendBusy,
1159 isTurnRunning,1154 phase,
1160 prompt,1155 prompt,
1161 showPlanFollowUpPrompt,1156 showPlanFollowUpPrompt,
1162 ]);1157 ]);
...@@ -2316,10 +2311,10 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2316,10 +2311,10 @@ export default function ChatView({ threadId }: ChatViewProps) {
2316 scheduleStickToBottom();2311 scheduleStickToBottom();
2317 }, [messageCount, scheduleStickToBottom]);2312 }, [messageCount, scheduleStickToBottom]);
2318 useEffect(() => {2313 useEffect(() => {
2319 if (!isTurnRunning) return;2314 if (phase !== "running") return;
2320 if (!shouldAutoScrollRef.current) return;2315 if (!shouldAutoScrollRef.current) return;
2321 scheduleStickToBottom();2316 scheduleStickToBottom();
2322 }, [isTurnRunning, scheduleStickToBottom, timelineEntries]);2317 }, [phase, scheduleStickToBottom, timelineEntries]);
23232318
2324 useEffect(() => {2319 useEffect(() => {
2325 setExpandedWorkGroups({});2320 setExpandedWorkGroups({});
...@@ -2538,14 +2533,14 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2538,14 +2533,14 @@ export default function ChatView({ threadId }: ChatViewProps) {
2538 : "local";2533 : "local";
25392534
2540 useEffect(() => {2535 useEffect(() => {
2541 if (!isTurnRunning) return;2536 if (phase !== "running") return;
2542 const timer = window.setInterval(() => {2537 const timer = window.setInterval(() => {
2543 setNowTick(Date.now());2538 setNowTick(Date.now());
2544 }, 1000);2539 }, 1000);
2545 return () => {2540 return () => {
2546 window.clearInterval(timer);2541 window.clearInterval(timer);
2547 };2542 };
2548 }, [isTurnRunning]);2543 }, [phase]);
25492544
2550 useEffect(() => {2545 useEffect(() => {
2551 if (!activeThreadId) return;2546 if (!activeThreadId) return;
...@@ -2765,7 +2760,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2765,7 +2760,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
2765 const api = readNativeApi();2760 const api = readNativeApi();
2766 if (!api || !activeThread || isRevertingCheckpoint) return;2761 if (!api || !activeThread || isRevertingCheckpoint) return;
27672762
2768 if (isTurnRunning || isSendBusy || isConnecting) {2763 if (phase === "running" || isSendBusy || isConnecting) {
2769 setThreadError(activeThread.id, "Interrupt the current turn before reverting checkpoints.");2764 setThreadError(activeThread.id, "Interrupt the current turn before reverting checkpoints.");
2770 return;2765 return;
2771 }2766 }
...@@ -2798,7 +2793,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2798,7 +2793,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
2798 }2793 }
2799 setIsRevertingCheckpoint(false);2794 setIsRevertingCheckpoint(false);
2800 },2795 },
2801 [activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, isTurnRunning, setThreadError],2796 [activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, phase, setThreadError],
2802 );2797 );
28032798
2804 const onSend = async (e?: { preventDefault: () => void }) => {2799 const onSend = async (e?: { preventDefault: () => void }) => {
...@@ -4387,7 +4382,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -4387,7 +4382,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
4387 }4382 }
4388 : null4383 : null
4389 }4384 }
4390 isRunning={isTurnRunning}4385 isRunning={phase === "running"}
4391 showPlanFollowUpPrompt={4386 showPlanFollowUpPrompt={
4392 pendingUserInputs.length === 0 && showPlanFollowUpPrompt4387 pendingUserInputs.length === 0 && showPlanFollowUpPrompt
4393 }4388 }
apps/web/src/session-logic.test.ts+3-85
...@@ -20,7 +20,6 @@ import {...@@ -20,7 +20,6 @@ import {
20 findSidebarProposedPlan,20 findSidebarProposedPlan,
21 hasActionableProposedPlan,21 hasActionableProposedPlan,
22 hasToolActivityForTurn,22 hasToolActivityForTurn,
23 isSessionActivelyRunningTurn,
24 isLatestTurnSettled,23 isLatestTurnSettled,
25} from "./session-logic";24} from "./session-logic";
2625
...@@ -1073,30 +1072,15 @@ describe("isLatestTurnSettled", () => {...@@ -1073,30 +1072,15 @@ describe("isLatestTurnSettled", () => {
1073 } as const;1072 } as const;
10741073
1075 it("returns false while the same turn is still active in a running session", () => {1074 it("returns false while the same turn is still active in a running session", () => {
1076 expect(
1077 isLatestTurnSettled(
1078 {
1079 ...latestTurn,
1080 completedAt: null,
1081 },
1082 {
1083 orchestrationStatus: "running",
1084 activeTurnId: TurnId.makeUnsafe("turn-1"),
1085 },
1086 ),
1087 ).toBe(false);
1088 });
1089
1090 it("returns true when the turn completed but the session status stayed stale-running", () => {
1091 expect(1075 expect(
1092 isLatestTurnSettled(latestTurn, {1076 isLatestTurnSettled(latestTurn, {
1093 orchestrationStatus: "running",1077 orchestrationStatus: "running",
1094 activeTurnId: TurnId.makeUnsafe("turn-1"),1078 activeTurnId: TurnId.makeUnsafe("turn-1"),
1095 }),1079 }),
1096 ).toBe(true);1080 ).toBe(false);
1097 });1081 });
10981082
1099 it("returns false while a different turn is still running", () => {1083 it("returns false while any turn is running to avoid stale latest-turn banners", () => {
1100 expect(1084 expect(
1101 isLatestTurnSettled(latestTurn, {1085 isLatestTurnSettled(latestTurn, {
1102 orchestrationStatus: "running",1086 orchestrationStatus: "running",
...@@ -1128,56 +1112,6 @@ describe("isLatestTurnSettled", () => {...@@ -1128,56 +1112,6 @@ describe("isLatestTurnSettled", () => {
1128 });1112 });
1129});1113});
11301114
1131describe("isSessionActivelyRunningTurn", () => {
1132 const completedTurn = {
1133 turnId: TurnId.makeUnsafe("turn-1"),
1134 startedAt: "2026-02-27T21:10:00.000Z",
1135 completedAt: "2026-02-27T21:10:06.000Z",
1136 } as const;
1137
1138 it("returns true when the current turn has not completed yet", () => {
1139 expect(
1140 isSessionActivelyRunningTurn(
1141 {
1142 ...completedTurn,
1143 completedAt: null,
1144 },
1145 {
1146 orchestrationStatus: "running",
1147 activeTurnId: TurnId.makeUnsafe("turn-1"),
1148 },
1149 ),
1150 ).toBe(true);
1151 });
1152
1153 it("returns false when the same turn already completed and only the session is stale", () => {
1154 expect(
1155 isSessionActivelyRunningTurn(completedTurn, {
1156 orchestrationStatus: "running",
1157 activeTurnId: TurnId.makeUnsafe("turn-1"),
1158 }),
1159 ).toBe(false);
1160 });
1161
1162 it("returns true when a different turn is still active", () => {
1163 expect(
1164 isSessionActivelyRunningTurn(completedTurn, {
1165 orchestrationStatus: "running",
1166 activeTurnId: TurnId.makeUnsafe("turn-2"),
1167 }),
1168 ).toBe(true);
1169 });
1170
1171 it("returns false when the session is not running", () => {
1172 expect(
1173 isSessionActivelyRunningTurn(completedTurn, {
1174 orchestrationStatus: "ready",
1175 activeTurnId: undefined,
1176 }),
1177 ).toBe(false);
1178 });
1179});
1180
1181describe("deriveActiveWorkStartedAt", () => {1115describe("deriveActiveWorkStartedAt", () => {
1182 const latestTurn = {1116 const latestTurn = {
1183 turnId: TurnId.makeUnsafe("turn-1"),1117 turnId: TurnId.makeUnsafe("turn-1"),
...@@ -1186,22 +1120,6 @@ describe("deriveActiveWorkStartedAt", () => {...@@ -1186,22 +1120,6 @@ describe("deriveActiveWorkStartedAt", () => {
1186 } as const;1120 } as const;
11871121
1188 it("prefers the in-flight turn start when the latest turn is not settled", () => {1122 it("prefers the in-flight turn start when the latest turn is not settled", () => {
1189 expect(
1190 deriveActiveWorkStartedAt(
1191 {
1192 ...latestTurn,
1193 completedAt: null,
1194 },
1195 {
1196 orchestrationStatus: "running",
1197 activeTurnId: TurnId.makeUnsafe("turn-1"),
1198 },
1199 "2026-02-27T21:11:00.000Z",
1200 ),
1201 ).toBe("2026-02-27T21:10:00.000Z");
1202 });
1203
1204 it("falls back to sendStartedAt when only the session status is stale-running", () => {
1205 expect(1123 expect(
1206 deriveActiveWorkStartedAt(1124 deriveActiveWorkStartedAt(
1207 latestTurn,1125 latestTurn,
...@@ -1211,7 +1129,7 @@ describe("deriveActiveWorkStartedAt", () => {...@@ -1211,7 +1129,7 @@ describe("deriveActiveWorkStartedAt", () => {
1211 },1129 },
1212 "2026-02-27T21:11:00.000Z",1130 "2026-02-27T21:11:00.000Z",
1213 ),1131 ),
1214 ).toBe("2026-02-27T21:11:00.000Z");1132 ).toBe("2026-02-27T21:10:00.000Z");
1215 });1133 });
12161134
1217 it("falls back to sendStartedAt once the latest turn is settled", () => {1135 it("falls back to sendStartedAt once the latest turn is settled", () => {
apps/web/src/session-logic.ts+3-18
...@@ -128,30 +128,15 @@ export function formatElapsed(startIso: string, endIso: string | undefined): str...@@ -128,30 +128,15 @@ export function formatElapsed(startIso: string, endIso: string | undefined): str
128type LatestTurnTiming = Pick<OrchestrationLatestTurn, "turnId" | "startedAt" | "completedAt">;128type LatestTurnTiming = Pick<OrchestrationLatestTurn, "turnId" | "startedAt" | "completedAt">;
129type SessionActivityState = Pick<ThreadSession, "orchestrationStatus" | "activeTurnId">;129type SessionActivityState = Pick<ThreadSession, "orchestrationStatus" | "activeTurnId">;
130130
131export function isSessionActivelyRunningTurn(
132 latestTurn: LatestTurnTiming | null,
133 session: SessionActivityState | null,
134): boolean {
135 if (!session || session.orchestrationStatus !== "running") return false;
136 if (!latestTurn) return true;
137
138 const activeTurnId = session.activeTurnId;
139 if (activeTurnId === undefined) {
140 return latestTurn.completedAt === null;
141 }
142 if (latestTurn.turnId !== activeTurnId) {
143 return true;
144 }
145 return latestTurn.completedAt === null;
146}
147
148export function isLatestTurnSettled(131export function isLatestTurnSettled(
149 latestTurn: LatestTurnTiming | null,132 latestTurn: LatestTurnTiming | null,
150 session: SessionActivityState | null,133 session: SessionActivityState | null,
151): boolean {134): boolean {
152 if (!latestTurn?.startedAt) return false;135 if (!latestTurn?.startedAt) return false;
153 if (!latestTurn.completedAt) return false;136 if (!latestTurn.completedAt) return false;
154 return !isSessionActivelyRunningTurn(latestTurn, session);137 if (!session) return true;
138 if (session.orchestrationStatus === "running") return false;
139 return true;
155}140}
156141
157export function deriveActiveWorkStartedAt(142export function deriveActiveWorkStartedAt(