diff --git a/.server-changes/sessions-test-column.md b/.server-changes/sessions-test-column.md new file mode 100644 index 00000000000..4e28034b2bc --- /dev/null +++ b/.server-changes/sessions-test-column.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: feature +--- + +Agent sessions started from the Test playground are now flagged with a real `Session.isTest` boolean instead of a `"playground"` tag, surfaced as a dedicated "Test" column (check icon) in the Sessions table on both the Sessions and Agent pages, plus a matching property on the session detail page. The legacy `"playground"` tag is hidden from the Tags display on pre-existing sessions. diff --git a/apps/webapp/app/components/sessions/v1/SessionsTable.tsx b/apps/webapp/app/components/sessions/v1/SessionsTable.tsx index ac9b5edbde8..eb6475d3e25 100644 --- a/apps/webapp/app/components/sessions/v1/SessionsTable.tsx +++ b/apps/webapp/app/components/sessions/v1/SessionsTable.tsx @@ -1,4 +1,5 @@ import { ArrowRightIcon } from "@heroicons/react/20/solid"; +import { CheckIcon } from "@heroicons/react/24/solid"; import { useLocation, useNavigation } from "@remix-run/react"; import { formatDuration } from "@trigger.dev/core/v3/utils/durations"; import { RunsIconExtraSmall } from "~/assets/icons/RunsIcon"; @@ -87,6 +88,7 @@ export function SessionsTable({ Type Agent ID + Test Tags Created Duration @@ -97,7 +99,7 @@ export function SessionsTable({ {sessions.length === 0 ? ( - +
{hasFilters @@ -144,6 +146,19 @@ export function SessionsTable({
+ + {session.isTest ? "Yes" : "No"} + {session.isTest ? ( + + ) : ( + + – + + )} + {session.tags.length > 0 ? (
@@ -168,7 +183,7 @@ export function SessionsTable({ )} {isLoading && ( Loading… diff --git a/apps/webapp/app/presenters/v3/SessionListPresenter.server.ts b/apps/webapp/app/presenters/v3/SessionListPresenter.server.ts index bff1bda0177..a5bcd37744b 100644 --- a/apps/webapp/app/presenters/v3/SessionListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/SessionListPresenter.server.ts @@ -7,6 +7,7 @@ import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server"; import { type SessionStatus, SessionsRepository, + LEGACY_PLAYGROUND_TAG, } from "~/services/sessionsRepository/sessionsRepository.server"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server"; @@ -225,7 +226,13 @@ export class SessionListPresenter { externalId: session.externalId, type: session.type, taskIdentifier: session.taskIdentifier, - tags: session.tags ? [...session.tags].sort((a, b) => a.localeCompare(b)) : [], + isTest: session.isTest, + // Hide the legacy "playground" tag (pre-isTest sessions) from display. + tags: session.tags + ? [...session.tags] + .filter((t) => t !== LEGACY_PLAYGROUND_TAG) + .sort((a, b) => a.localeCompare(b)) + : [], status, closedAt: session.closedAt ? session.closedAt.toISOString() : undefined, closedReason: session.closedReason ?? undefined, diff --git a/apps/webapp/app/presenters/v3/SessionPresenter.server.ts b/apps/webapp/app/presenters/v3/SessionPresenter.server.ts index 36ef46d4b4e..7b4183426c8 100644 --- a/apps/webapp/app/presenters/v3/SessionPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/SessionPresenter.server.ts @@ -4,6 +4,7 @@ import { env } from "~/env.server"; import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server"; import { chatSnapshotStorageKey } from "~/services/realtime/chatSnapshot.server"; import { resolveSessionByIdOrExternalId } from "~/services/realtime/sessions.server"; +import { LEGACY_PLAYGROUND_TAG } from "~/services/sessionsRepository/sessionsRepository.server"; import { logger } from "~/services/logger.server"; import { generatePresignedUrl } from "~/v3/objectStore.server"; import { runStore } from "~/v3/runStore.server"; @@ -177,7 +178,13 @@ export class SessionPresenter { externalId: session.externalId, type: session.type, taskIdentifier: session.taskIdentifier, - tags: session.tags ? [...session.tags].sort((a, b) => a.localeCompare(b)) : [], + isTest: session.isTest, + // Hide the legacy "playground" tag (pre-isTest sessions) from display. + tags: session.tags + ? [...session.tags] + .filter((t) => t !== LEGACY_PLAYGROUND_TAG) + .sort((a, b) => a.localeCompare(b)) + : [], metadata: session.metadata, triggerConfig: session.triggerConfig, streamBasinName: session.streamBasinName, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx index 2bf62b8cad2..b9bd387159b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx @@ -1,5 +1,5 @@ import { BoltIcon, BoltSlashIcon } from "@heroicons/react/20/solid"; -import { BookOpenIcon } from "@heroicons/react/24/solid"; +import { BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid"; import { type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { useVirtualizer } from "@tanstack/react-virtual"; @@ -818,6 +818,19 @@ function OverviewTab({ session, status }: { session: LoadedSession; status: Sess {session.taskIdentifier} + + Test + + {session.isTest ? "Yes" : "No"} + {session.isTest ? ( + + ) : ( + + – + + )} + + {session.currentRun ? ( Current run diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.action.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.action.tsx index da77d2cc692..826618277af 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.action.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.action.tsx @@ -162,7 +162,10 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { type: "chat.agent", taskIdentifier: agentSlug, triggerConfig: triggerConfig as unknown as Prisma.InputJsonValue, - tags: ["playground"], + // Mark as a Test session — surfaced via the Test column in the + // Sessions table. Session tags stay empty; the triggered run still + // carries "playground:true" via triggerConfig.tags. + isTest: true, projectId: project.id, runtimeEnvironmentId: environment.id, environmentType: environment.type, diff --git a/apps/webapp/app/services/sessionsReplicationService.server.ts b/apps/webapp/app/services/sessionsReplicationService.server.ts index 12b66e29dfb..da202458ca8 100644 --- a/apps/webapp/app/services/sessionsReplicationService.server.ts +++ b/apps/webapp/app/services/sessionsReplicationService.server.ts @@ -802,6 +802,7 @@ function toSessionInsertArray( session.expiresAt ? session.expiresAt.getTime() : null, session.createdAt.getTime(), session.updatedAt.getTime(), + session.isTest ?? false, version.toString(), isDeleted ? 1 : 0, ]; diff --git a/apps/webapp/app/services/sessionsRepository/clickhouseSessionsRepository.server.ts b/apps/webapp/app/services/sessionsRepository/clickhouseSessionsRepository.server.ts index aebf61628fa..b66faf4d3e0 100644 --- a/apps/webapp/app/services/sessionsRepository/clickhouseSessionsRepository.server.ts +++ b/apps/webapp/app/services/sessionsRepository/clickhouseSessionsRepository.server.ts @@ -93,6 +93,7 @@ export class ClickHouseSessionsRepository implements ISessionsRepository { externalId: true, type: true, taskIdentifier: true, + isTest: true, tags: true, metadata: true, closedAt: true, diff --git a/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts b/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts index 245f1df2295..5a808051035 100644 --- a/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts +++ b/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts @@ -24,6 +24,13 @@ export type SessionsRepositoryOptions = { export const SessionStatus = z.enum(["ACTIVE", "CLOSED", "EXPIRED"]); export type SessionStatus = z.infer; +/** + * Legacy marker tag for sessions created from the Test/playground before the + * `Session.isTest` boolean existed. New sessions set `isTest` instead; this tag + * is hidden from the Tags display so it doesn't surface on pre-isTest rows. + */ +export const LEGACY_PLAYGROUND_TAG = "playground"; + const SessionListInputOptionsSchema = z.object({ organizationId: z.string(), projectId: z.string(), @@ -87,6 +94,7 @@ export type ListedSession = Prisma.SessionGetPayload<{ externalId: true; type: true; taskIdentifier: true; + isTest: true; tags: true; metadata: true; closedAt: true; diff --git a/apps/webapp/test/sessionsReplicationService.test.ts b/apps/webapp/test/sessionsReplicationService.test.ts index 1d3c761e813..cb171ed5b54 100644 --- a/apps/webapp/test/sessionsReplicationService.test.ts +++ b/apps/webapp/test/sessionsReplicationService.test.ts @@ -80,6 +80,7 @@ describe("SessionsReplicationService", () => { }, tags: ["user:42", "plan:pro"], metadata: { plan: "pro", seats: 3 }, + isTest: true, }, }); @@ -108,6 +109,7 @@ describe("SessionsReplicationService", () => { environment_type: "DEVELOPMENT", task_identifier: "my-agent", tags: ["user:42", "plan:pro"], + is_test: 1, _is_deleted: 0, }) ); diff --git a/internal-packages/clickhouse/schema/034_add_is_test_to_sessions_v1.sql b/internal-packages/clickhouse/schema/034_add_is_test_to_sessions_v1.sql new file mode 100644 index 00000000000..8d2f46e1249 --- /dev/null +++ b/internal-packages/clickhouse/schema/034_add_is_test_to_sessions_v1.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- Existing rows default to 0 and are intentionally NOT backfilled: the Sessions +-- list reads isTest from Postgres (ClickHouse only supplies session IDs), so the +-- UI is correct without it. A backfill is only needed if a ClickHouse-side +-- isTest filter/aggregate over sessions_v1 is added later. +ALTER TABLE trigger_dev.sessions_v1 + ADD COLUMN IF NOT EXISTS is_test UInt8 DEFAULT 0; + +-- +goose Down +ALTER TABLE trigger_dev.sessions_v1 + DROP COLUMN IF EXISTS is_test; diff --git a/internal-packages/clickhouse/src/sessions.ts b/internal-packages/clickhouse/src/sessions.ts index 567fe65511e..995e98447e0 100644 --- a/internal-packages/clickhouse/src/sessions.ts +++ b/internal-packages/clickhouse/src/sessions.ts @@ -19,6 +19,7 @@ export const SessionV1 = z.object({ expires_at: z.number().int().nullish(), created_at: z.number().int(), updated_at: z.number().int(), + is_test: z.boolean().default(false), _version: z.string(), _is_deleted: z.number().int().default(0), }); @@ -43,6 +44,7 @@ export const SESSION_COLUMNS = [ "expires_at", "created_at", "updated_at", + "is_test", "_version", "_is_deleted", ] as const; @@ -70,6 +72,7 @@ export type SessionFieldTypes = { expires_at: number | null; created_at: number; updated_at: number; + is_test: boolean; _version: string; _is_deleted: number; }; @@ -95,6 +98,7 @@ export type SessionInsertArray = [ expires_at: number | null, created_at: number, updated_at: number, + is_test: boolean, _version: string, _is_deleted: number, ]; diff --git a/internal-packages/database/prisma/migrations/20260621000000_add_is_test_to_session/migration.sql b/internal-packages/database/prisma/migrations/20260621000000_add_is_test_to_session/migration.sql new file mode 100644 index 00000000000..5e50a167f21 --- /dev/null +++ b/internal-packages/database/prisma/migrations/20260621000000_add_is_test_to_session/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Session" ADD COLUMN "isTest" BOOLEAN NOT NULL DEFAULT false; diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index bb80da3a7ec..9658e664347 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -805,6 +805,10 @@ model Session { /// (chatId, messages, trigger) are merged at trigger time. triggerConfig Json + /// Whether this session was created from the Test/playground UI rather + /// than by a real chat.agent() trigger. Mirrors TaskRun.isTest. + isTest Boolean @default(false) + tags String[] @default([]) metadata Json?