Skip to content

Commit 7764abc

Browse files
committed
fix(clients): a running session shows Working only while a turn is active
1 parent bfc5631 commit 7764abc

9 files changed

Lines changed: 84 additions & 8 deletions

File tree

apps/mobile/src/features/threads/ThreadComposer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
837837
props.connectionState === "connected" &&
838838
composerAuthority.providerAdmissionAvailable &&
839839
props.selectedThread.session?.status === "running" &&
840+
props.selectedThread.session.activeTurnId != null &&
840841
!props.sessionInputBlocked &&
841842
props.localOutboxCount === 0 &&
842843
supportsSessionInputQueueFollowUp(activeSessionProviderStatus);

apps/mobile/src/features/threads/threadListV2.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,29 @@ describe("resolveThreadListV2Status", () => {
218218
});
219219
});
220220

221+
describe("resolveThreadListV2Status running without a turn", () => {
222+
it("does not report working for a running session with no active turn", () => {
223+
expect(
224+
resolveThreadListV2Status(
225+
makeThread({
226+
id: ThreadId.make("t"),
227+
title: "t",
228+
session: {
229+
threadId: ThreadId.make("t"),
230+
status: "running",
231+
providerName: "Codex",
232+
providerInstanceId: ProviderInstanceId.make("codex"),
233+
runtimeMode: "full-access",
234+
activeTurnId: null,
235+
lastError: null,
236+
updatedAt: NOW,
237+
},
238+
}),
239+
),
240+
).toBe("ready");
241+
});
242+
});
243+
221244
describe("resolveThreadListV2SwipeActions", () => {
222245
it("offers settle and snooze for an active snoozable thread", () => {
223246
expect(

apps/mobile/src/features/threads/threadListV2.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ function isThreadListV2LatestTurnSettled(
133133
thread: Pick<EnvironmentThreadShell, "latestTurn" | "session">,
134134
): boolean {
135135
if (!thread.latestTurn?.startedAt || !thread.latestTurn.completedAt) return false;
136-
return thread.session?.status !== "running";
136+
// A running session with no active turn has nothing in flight.
137+
return !(thread.session?.status === "running" && thread.session.activeTurnId != null);
137138
}
138139

139140
export function resolveThreadListV2Status(
@@ -153,7 +154,12 @@ export function resolveThreadListV2Status(
153154
if (thread.hasPendingUserInput) {
154155
return "input";
155156
}
156-
if (thread.session?.status === "running" || thread.session?.status === "starting") {
157+
// "running" alone is not work: a provider can report it between turns
158+
// (Claude system/status) with nothing in flight. Only an active turn is.
159+
if (
160+
thread.session?.status === "starting" ||
161+
(thread.session?.status === "running" && thread.session.activeTurnId != null)
162+
) {
157163
return "working";
158164
}
159165
if (thread.session?.status === "error") {

apps/mobile/src/features/threads/threadPresentation.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,22 @@ describe("resolveThreadStatus", () => {
4848
});
4949
});
5050

51+
it("shows no status for a running session with no active turn", () => {
52+
expect(
53+
resolveThreadStatus({
54+
...baseThread,
55+
session: { status: "running", activeTurnId: null },
56+
} as EnvironmentThreadShell),
57+
).toBeNull();
58+
});
59+
5160
it.each(["running", "starting"] as const)(
5261
"uses upstream sky while the session is %s",
5362
(status) => {
5463
expect(
5564
resolveThreadStatus({
5665
...baseThread,
57-
session: { status },
66+
session: status === "running" ? { status, activeTurnId: "turn-1" } : { status },
5867
} as EnvironmentThreadShell),
5968
).toMatchObject({
6069
pillClassName: "bg-adaptive-sky-500-a12-a16",

apps/mobile/src/features/threads/threadPresentation.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ function isLatestTurnSettled(
2727
if (!latestTurn?.startedAt) return false;
2828
if (!latestTurn.completedAt) return false;
2929
if (!session) return true;
30-
return session.status !== "running";
30+
// A running session with no active turn has nothing in flight.
31+
return !(session.status === "running" && session.activeTurnId != null);
3132
}
3233

3334
/**
@@ -62,7 +63,7 @@ export function resolveThreadStatus(
6263
};
6364
}
6465

65-
if (thread.session?.status === "running") {
66+
if (thread.session?.status === "running" && thread.session.activeTurnId != null) {
6667
return {
6768
kind: "working",
6869
label: "Working",

apps/web/src/components/Sidebar.logic.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,18 @@ describe("resolveSidebarThreadStatus", () => {
707707
).toBe("working");
708708
});
709709

710+
it("does not report working for a running session with no active turn", () => {
711+
// A provider can report running between turns (Claude system/status).
712+
// Without a turn there is nothing being worked on; background liveness,
713+
// if any, is the honest signal.
714+
expect(
715+
resolveSidebarThreadStatus({
716+
...idle,
717+
session: { ...session, activeTurnId: null },
718+
}),
719+
).toBe("ready");
720+
});
721+
710722
it("distinguishes background delegation from root work", () => {
711723
expect(
712724
resolveSidebarThreadStatus({
@@ -1223,6 +1235,14 @@ describe("resolveThreadStatusPill", () => {
12231235
});
12241236
});
12251237

1238+
it("shows no pill for a running session with no active turn", () => {
1239+
expect(
1240+
resolveThreadStatusPill({
1241+
thread: { ...baseThread, session: { ...baseThread.session, activeTurnId: null } },
1242+
}),
1243+
).toBeNull();
1244+
});
1245+
12261246
it("uses upstream sky pulses for running and connecting", () => {
12271247
expect(resolveThreadStatusPill({ thread: baseThread })).toMatchObject({
12281248
label: "Working",

apps/web/src/components/Sidebar.logic.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,12 @@ export function resolveSidebarThreadStatus(thread: SidebarThreadStatusInput): Si
573573
if (thread.hasPendingUserInput) {
574574
return "input";
575575
}
576-
if (thread.session?.status === "running" || thread.session?.status === "starting") {
576+
// "running" alone is not work: a provider can report it between turns
577+
// (Claude system/status) with nothing in flight. Only an active turn is.
578+
if (
579+
thread.session?.status === "starting" ||
580+
(thread.session?.status === "running" && thread.session.activeTurnId != null)
581+
) {
577582
return "working";
578583
}
579584
// A failed session outranks lingering background liveness: the user must
@@ -801,7 +806,7 @@ export function resolveThreadStatusPill(input: {
801806
};
802807
}
803808

804-
if (thread.session?.status === "running") {
809+
if (thread.session?.status === "running" && thread.session.activeTurnId != null) {
805810
return {
806811
label: "Working",
807812
colorClass: "text-sky-600 dark:text-sky-300/80",

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

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

2226+
it("returns true when the session reports running but tracks no turn", () => {
2227+
expect(
2228+
isLatestTurnSettled(latestTurn, {
2229+
status: "running",
2230+
activeTurnId: null,
2231+
}),
2232+
).toBe(true);
2233+
});
2234+
22262235
it("returns true once the session is no longer running that turn", () => {
22272236
expect(
22282237
isLatestTurnSettled(latestTurn, {

apps/web/src/session-logic.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,9 @@ export function isLatestTurnSettled(
398398
if (!latestTurn?.startedAt) return false;
399399
if (!latestTurn.completedAt) return false;
400400
if (!session) return true;
401-
if (session.status === "running") return false;
401+
// A running session with no active turn has nothing in flight; the latest
402+
// turn is as settled as it will get.
403+
if (session.status === "running" && session.activeTurnId != null) return false;
402404
return true;
403405
}
404406

0 commit comments

Comments
 (0)