fix(webapp): keep the close action available for idle sessions

Address review on the sessions status change:
- Keep the "Close session" action on the detail page for Idle sessions; they
  are open, only Closed and Expired are terminal.
- Rename the status helper input from currentRunId to hasCurrentRun, since the
  detail page passes a run friendlyId, not the session's currentRunId.
- Restore the Active tooltip copy so it stays accurate now that the Active
  filter also returns open, idle sessions.
- Align the sessions docs example so the listed tag matches a top-level tag
  set at start time.
This commit is contained in:
D-K-P
2026-08-11 16:31:59 +01:00
parent 799dbb4758
commit e67dc2b6eb
6 changed files with 20 additions and 17 deletions
@@ -13,7 +13,7 @@ export const allSessionStatuses = ["ACTIVE", "CLOSED", "EXPIRED"] as const satis
>;
const descriptions: Record<SessionDisplayStatus, string> = {
ACTIVE: "The session has a run currently executing.",
ACTIVE: "The session is open and can receive input or schedule new runs.",
IDLE: "The session is open but has no run currently executing.",
CLOSED: "The session was closed; no further input or runs can be triggered against it.",
EXPIRED: "The session passed its expiry time without being closed explicitly.",
@@ -214,7 +214,7 @@ export class SessionListPresenter {
const status = deriveSessionStatus({
closedAt: session.closedAt,
expiresAt: session.expiresAt,
currentRunId: session.currentRunId,
hasCurrentRun: session.currentRunId != null,
currentRunStatus: currentRun?.status,
now,
});
@@ -11,7 +11,7 @@ describe("deriveSessionStatus", () => {
deriveSessionStatus({
closedAt: PAST,
expiresAt: null,
currentRunId: "run_1",
hasCurrentRun: true,
currentRunStatus: "EXECUTING",
now: NOW,
})
@@ -23,7 +23,7 @@ describe("deriveSessionStatus", () => {
deriveSessionStatus({
closedAt: PAST,
expiresAt: PAST,
currentRunId: null,
hasCurrentRun: false,
currentRunStatus: undefined,
now: NOW,
})
@@ -35,7 +35,7 @@ describe("deriveSessionStatus", () => {
deriveSessionStatus({
closedAt: null,
expiresAt: PAST,
currentRunId: "run_1",
hasCurrentRun: true,
currentRunStatus: "EXECUTING",
now: NOW,
})
@@ -47,7 +47,7 @@ describe("deriveSessionStatus", () => {
deriveSessionStatus({
closedAt: null,
expiresAt: FUTURE,
currentRunId: "run_1",
hasCurrentRun: true,
currentRunStatus: "EXECUTING",
now: NOW,
})
@@ -59,7 +59,7 @@ describe("deriveSessionStatus", () => {
deriveSessionStatus({
closedAt: null,
expiresAt: null,
currentRunId: "run_1",
hasCurrentRun: true,
currentRunStatus: "EXPIRED",
now: NOW,
})
@@ -71,7 +71,7 @@ describe("deriveSessionStatus", () => {
deriveSessionStatus({
closedAt: null,
expiresAt: null,
currentRunId: null,
hasCurrentRun: false,
currentRunStatus: undefined,
now: NOW,
})
@@ -83,7 +83,7 @@ describe("deriveSessionStatus", () => {
deriveSessionStatus({
closedAt: null,
expiresAt: null,
currentRunId: "run_missing",
hasCurrentRun: true,
currentRunStatus: undefined,
now: NOW,
})
@@ -7,11 +7,11 @@ export type DeriveSessionStatusInput = {
closedAt: Date | null;
/** `Session.expiresAt` — retention deadline, if any. */
expiresAt: Date | null;
/** `Session.currentRunId` — pointer to the current run (no FK). */
currentRunId: string | null;
/** Whether the session points at a current run at all. */
hasCurrentRun: boolean;
/**
* Status of the run named by `currentRunId`. `undefined` when there is no
* current run, or the pointer couldn't be resolved (stale / cross-env).
* Status of the current run. `undefined` when there is no current run, or the
* pointer couldn't be resolved (stale / cross-env).
*/
currentRunStatus: TaskRunStatus | undefined;
/** `Date.now()` at the time of derivation. */
@@ -38,7 +38,7 @@ export function deriveSessionStatus(input: DeriveSessionStatusInput): SessionDis
}
const hasLiveRun =
input.currentRunId != null &&
input.hasCurrentRun &&
input.currentRunStatus !== undefined &&
!isFinalRunStatus(input.currentRunStatus);
@@ -119,7 +119,7 @@ export default function Page() {
const status = deriveSessionStatus({
closedAt: session.closedAt ? new Date(session.closedAt) : null,
expiresAt: session.expiresAt ? new Date(session.expiresAt) : null,
currentRunId: session.currentRun?.friendlyId ?? null,
hasCurrentRun: session.currentRun != null,
currentRunStatus: session.currentRun?.status,
now: Date.now(),
});
@@ -791,7 +791,7 @@ function OverviewTab({
<SessionStatusCombo status={status} />
</Property.Value>
</Property.Item>
{status === "ACTIVE" && (
{(status === "ACTIVE" || status === "IDLE") && (
<Dialog key={`close-${session.friendlyId}`}>
<DialogTrigger asChild>
<Button variant="danger/small">Close session</Button>
+4 -1
View File
@@ -99,7 +99,10 @@ const { id, runId, publicAccessToken, isCached } = await sessions.start({
type: "chat.agent",
externalId: chatId,
taskIdentifier: "my-chat",
// Top-level tags live on the Session row and are what `sessions.list({ tag })` filters on.
tags: [`chat:${chatId}`],
triggerConfig: {
// triggerConfig.tags tag each run the session schedules, not the session row.
tags: [`chat:${chatId}`],
basePayload: { /* whatever your task's payload shape is */ },
},
@@ -153,7 +156,7 @@ Cursor-paginated list of Sessions in the current environment. Returns a `CursorP
```ts
for await (const s of sessions.list({
type: "chat.agent",
tag: `user:${userId}`,
tag: `chat:${chatId}`,
status: "ACTIVE",
limit: 50,
})) {