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
1 change: 1 addition & 0 deletions apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
props.connectionState === "connected" &&
composerAuthority.providerAdmissionAvailable &&
props.selectedThread.session?.status === "running" &&
props.selectedThread.session.activeTurnId != null &&
!props.sessionInputBlocked &&
props.localOutboxCount === 0 &&
supportsSessionInputQueueFollowUp(activeSessionProviderStatus);
Expand Down
23 changes: 23 additions & 0 deletions apps/mobile/src/features/threads/threadListV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,29 @@ describe("resolveThreadListV2Status", () => {
});
});

describe("resolveThreadListV2Status running without a turn", () => {
it("does not report working for a running session with no active turn", () => {
expect(
resolveThreadListV2Status(
makeThread({
id: ThreadId.make("t"),
title: "t",
session: {
threadId: ThreadId.make("t"),
status: "running",
providerName: "Codex",
providerInstanceId: ProviderInstanceId.make("codex"),
runtimeMode: "full-access",
activeTurnId: null,
lastError: null,
updatedAt: NOW,
},
}),
),
).toBe("ready");
});
});

describe("resolveThreadListV2SwipeActions", () => {
it("offers settle and snooze for an active snoozable thread", () => {
expect(
Expand Down
10 changes: 8 additions & 2 deletions apps/mobile/src/features/threads/threadListV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ function isThreadListV2LatestTurnSettled(
thread: Pick<EnvironmentThreadShell, "latestTurn" | "session">,
): boolean {
if (!thread.latestTurn?.startedAt || !thread.latestTurn.completedAt) return false;
return thread.session?.status !== "running";
// A running session with no active turn has nothing in flight.
return !(thread.session?.status === "running" && thread.session.activeTurnId != null);
}

export function resolveThreadListV2Status(
Expand All @@ -153,7 +154,12 @@ export function resolveThreadListV2Status(
if (thread.hasPendingUserInput) {
return "input";
}
if (thread.session?.status === "running" || thread.session?.status === "starting") {
// "running" alone is not work: a provider can report it between turns
// (Claude system/status) with nothing in flight. Only an active turn is.
if (
thread.session?.status === "starting" ||
(thread.session?.status === "running" && thread.session.activeTurnId != null)
) {
return "working";
}
if (thread.session?.status === "error") {
Expand Down
11 changes: 10 additions & 1 deletion apps/mobile/src/features/threads/threadPresentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,22 @@ describe("resolveThreadStatus", () => {
});
});

it("shows no status for a running session with no active turn", () => {
expect(
resolveThreadStatus({
...baseThread,
session: { status: "running", activeTurnId: null },
} as EnvironmentThreadShell),
).toBeNull();
});

it.each(["running", "starting"] as const)(
"uses upstream sky while the session is %s",
(status) => {
expect(
resolveThreadStatus({
...baseThread,
session: { status },
session: status === "running" ? { status, activeTurnId: "turn-1" } : { status },
} as EnvironmentThreadShell),
).toMatchObject({
pillClassName: "bg-adaptive-sky-500-a12-a16",
Expand Down
5 changes: 3 additions & 2 deletions apps/mobile/src/features/threads/threadPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ function isLatestTurnSettled(
if (!latestTurn?.startedAt) return false;
if (!latestTurn.completedAt) return false;
if (!session) return true;
return session.status !== "running";
// A running session with no active turn has nothing in flight.
return !(session.status === "running" && session.activeTurnId != null);
}

/**
Expand Down Expand Up @@ -62,7 +63,7 @@ export function resolveThreadStatus(
};
}

if (thread.session?.status === "running") {
if (thread.session?.status === "running" && thread.session.activeTurnId != null) {
return {
kind: "working",
label: "Working",
Expand Down
20 changes: 20 additions & 0 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,18 @@ describe("resolveSidebarThreadStatus", () => {
).toBe("working");
});

it("does not report working for a running session with no active turn", () => {
// A provider can report running between turns (Claude system/status).
// Without a turn there is nothing being worked on; background liveness,
// if any, is the honest signal.
expect(
resolveSidebarThreadStatus({
...idle,
session: { ...session, activeTurnId: null },
}),
).toBe("ready");
});

it("distinguishes background delegation from root work", () => {
expect(
resolveSidebarThreadStatus({
Expand Down Expand Up @@ -1223,6 +1235,14 @@ describe("resolveThreadStatusPill", () => {
});
});

it("shows no pill for a running session with no active turn", () => {
expect(
resolveThreadStatusPill({
thread: { ...baseThread, session: { ...baseThread.session, activeTurnId: null } },
}),
).toBeNull();
});

it("uses upstream sky pulses for running and connecting", () => {
expect(resolveThreadStatusPill({ thread: baseThread })).toMatchObject({
label: "Working",
Expand Down
9 changes: 7 additions & 2 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,12 @@ export function resolveSidebarThreadStatus(thread: SidebarThreadStatusInput): Si
if (thread.hasPendingUserInput) {
return "input";
}
if (thread.session?.status === "running" || thread.session?.status === "starting") {
// "running" alone is not work: a provider can report it between turns
// (Claude system/status) with nothing in flight. Only an active turn is.
if (
thread.session?.status === "starting" ||
(thread.session?.status === "running" && thread.session.activeTurnId != null)
) {
return "working";
}
// A failed session outranks lingering background liveness: the user must
Expand Down Expand Up @@ -801,7 +806,7 @@ export function resolveThreadStatusPill(input: {
};
}

if (thread.session?.status === "running") {
if (thread.session?.status === "running" && thread.session.activeTurnId != null) {
return {
label: "Working",
colorClass: "text-sky-600 dark:text-sky-300/80",
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 @@ -2223,6 +2223,15 @@ describe("isLatestTurnSettled", () => {
).toBe(false);
});

it("returns true when the session reports running but tracks no turn", () => {
expect(
isLatestTurnSettled(latestTurn, {
status: "running",
activeTurnId: null,
}),
).toBe(true);
});

it("returns true once the session is no longer running that turn", () => {
expect(
isLatestTurnSettled(latestTurn, {
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/session-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,9 @@ export function isLatestTurnSettled(
if (!latestTurn?.startedAt) return false;
if (!latestTurn.completedAt) return false;
if (!session) return true;
if (session.status === "running") return false;
// A running session with no active turn has nothing in flight; the latest
// turn is as settled as it will get.
if (session.status === "running" && session.activeTurnId != null) return false;
return true;
}

Expand Down
Loading