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 {
5252 deriveWorkLogEntries,
5353 hasActionableProposedPlan,
5454 hasToolActivityForTurn,
55 isSessionActivelyRunningTurn,
5655 isLatestTurnSettled,
5756 formatElapsed,
5857} from "../session-logic";
......@@ -1121,11 +1120,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
11211120 activePendingUserInput: activePendingUserInput?.requestId ?? null,
11221121 threadError: activeThread?.error,
11231122 });
1124 const isTurnRunning = isSessionActivelyRunningTurn(
1125 activeLatestTurn,
1126 activeThread?.session ?? null,
1127 );
1128 const isWorking = isTurnRunning || isSendBusy || isConnecting || isRevertingCheckpoint;
1123 const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint;
11291124 const nowIso = new Date(nowTick).toISOString();
11301125 const activeWorkStartedAt = deriveActiveWorkStartedAt(
11311126 activeLatestTurn,
......@@ -1142,7 +1137,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
11421137 if (activePendingProgress) {
11431138 return `pending:${activePendingProgress.questionIndex}:${activePendingProgress.isLastQuestion}:${activePendingIsResponding}`;
11441139 }
1145 if (isTurnRunning) {
1140 if (phase === "running") {
11461141 return "running";
11471142 }
11481143 if (showPlanFollowUpPrompt) {
......@@ -1156,7 +1151,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
11561151 isConnecting,
11571152 isPreparingWorktree,
11581153 isSendBusy,
1159 isTurnRunning,
1154 phase,
11601155 prompt,
11611156 showPlanFollowUpPrompt,
11621157 ]);
......@@ -2316,10 +2311,10 @@ export default function ChatView({ threadId }: ChatViewProps) {
23162311 scheduleStickToBottom();
23172312 }, [messageCount, scheduleStickToBottom]);
23182313 useEffect(() => {
2319 if (!isTurnRunning) return;
2314 if (phase !== "running") return;
23202315 if (!shouldAutoScrollRef.current) return;
23212316 scheduleStickToBottom();
2322 }, [isTurnRunning, scheduleStickToBottom, timelineEntries]);
2317 }, [phase, scheduleStickToBottom, timelineEntries]);
23232318
23242319 useEffect(() => {
23252320 setExpandedWorkGroups({});
......@@ -2538,14 +2533,14 @@ export default function ChatView({ threadId }: ChatViewProps) {
25382533 : "local";
25392534
25402535 useEffect(() => {
2541 if (!isTurnRunning) return;
2536 if (phase !== "running") return;
25422537 const timer = window.setInterval(() => {
25432538 setNowTick(Date.now());
25442539 }, 1000);
25452540 return () => {
25462541 window.clearInterval(timer);
25472542 };
2548 }, [isTurnRunning]);
2543 }, [phase]);
25492544
25502545 useEffect(() => {
25512546 if (!activeThreadId) return;
......@@ -2765,7 +2760,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
27652760 const api = readNativeApi();
27662761 if (!api || !activeThread || isRevertingCheckpoint) return;
27672762
2768 if (isTurnRunning || isSendBusy || isConnecting) {
2763 if (phase === "running" || isSendBusy || isConnecting) {
27692764 setThreadError(activeThread.id, "Interrupt the current turn before reverting checkpoints.");
27702765 return;
27712766 }
......@@ -2798,7 +2793,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
27982793 }
27992794 setIsRevertingCheckpoint(false);
28002795 },
2801 [activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, isTurnRunning, setThreadError],
2796 [activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, phase, setThreadError],
28022797 );
28032798
28042799 const onSend = async (e?: { preventDefault: () => void }) => {
......@@ -4387,7 +4382,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
43874382 }
43884383 : null
43894384 }
4390 isRunning={isTurnRunning}
4385 isRunning={phase === "running"}
43914386 showPlanFollowUpPrompt={
43924387 pendingUserInputs.length === 0 && showPlanFollowUpPrompt
43934388 }
apps/web/src/session-logic.test.ts+3-85
......@@ -20,7 +20,6 @@ import {
2020 findSidebarProposedPlan,
2121 hasActionableProposedPlan,
2222 hasToolActivityForTurn,
23 isSessionActivelyRunningTurn,
2423 isLatestTurnSettled,
2524} from "./session-logic";
2625
......@@ -1073,30 +1072,15 @@ describe("isLatestTurnSettled", () => {
10731072 } as const;
10741073
10751074 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", () => {
10911075 expect(
10921076 isLatestTurnSettled(latestTurn, {
10931077 orchestrationStatus: "running",
10941078 activeTurnId: TurnId.makeUnsafe("turn-1"),
10951079 }),
1096 ).toBe(true);
1080 ).toBe(false);
10971081 });
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", () => {
11001084 expect(
11011085 isLatestTurnSettled(latestTurn, {
11021086 orchestrationStatus: "running",
......@@ -1128,56 +1112,6 @@ describe("isLatestTurnSettled", () => {
11281112 });
11291113});
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
11811115describe("deriveActiveWorkStartedAt", () => {
11821116 const latestTurn = {
11831117 turnId: TurnId.makeUnsafe("turn-1"),
......@@ -1186,22 +1120,6 @@ describe("deriveActiveWorkStartedAt", () => {
11861120 } as const;
11871121
11881122 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", () => {
12051123 expect(
12061124 deriveActiveWorkStartedAt(
12071125 latestTurn,
......@@ -1211,7 +1129,7 @@ describe("deriveActiveWorkStartedAt", () => {
12111129 },
12121130 "2026-02-27T21:11:00.000Z",
12131131 ),
1214 ).toBe("2026-02-27T21:11:00.000Z");
1132 ).toBe("2026-02-27T21:10:00.000Z");
12151133 });
12161134
12171135 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
128128type LatestTurnTiming = Pick<OrchestrationLatestTurn, "turnId" | "startedAt" | "completedAt">;
129129type 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
148131export function isLatestTurnSettled(
149132 latestTurn: LatestTurnTiming | null,
150133 session: SessionActivityState | null,
151134): boolean {
152135 if (!latestTurn?.startedAt) return false;
153136 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;
155140}
156141
157142export function deriveActiveWorkStartedAt(