From 24c24f4300e2f9dde068d32405f2ecb3436f29f7 Mon Sep 17 00:00:00 2001
From: "@mrubens" <2600+mrubens@users.noreply.github.com>
Date: Fri, 4 Sep 2026 20:46:11 +0000
Subject: [PATCH 1/2] feat: add empty Fast Session launch
---
.../(authenticated)/home/Home.client.test.tsx | 18 ++++++
.../FastSessionTranscript.client.test.tsx | 26 +++++++++
.../[sessionId]/FastSessionTranscript.tsx | 14 ++++-
.../sessions/[sessionId]/page.test.tsx | 57 +++++++++++++++++++
.../(sandbox)/sessions/[sessionId]/page.tsx | 3 +
.../tasks/NewTaskDialog.client.test.tsx | 7 +++
.../src/components/tasks/NewTaskDialog.tsx | 1 +
apps/web/src/components/tasks/NewTaskForm.tsx | 56 +++++++++++++++---
.../hooks/task-runs/useStartFastSession.ts | 4 ++
.../trpc/commands/fast-sessions/index.test.ts | 36 ++++++++++++
.../src/trpc/commands/fast-sessions/index.ts | 12 ++++
.../trpc/commands/fast-sessions/input.test.ts | 25 ++++++++
.../src/trpc/commands/fast-sessions/input.ts | 24 +++++++-
13 files changed, 270 insertions(+), 13 deletions(-)
diff --git a/apps/web/src/app/(authenticated)/home/Home.client.test.tsx b/apps/web/src/app/(authenticated)/home/Home.client.test.tsx
index d78f50347..8d5a3b18f 100644
--- a/apps/web/src/app/(authenticated)/home/Home.client.test.tsx
+++ b/apps/web/src/app/(authenticated)/home/Home.client.test.tsx
@@ -352,6 +352,24 @@ describe('Home', () => {
await waitFor(() => expect(onTaskStarted).toHaveBeenCalledOnce());
});
+ it('starts an idempotent empty Session from the explicit dialog action', async () => {
+ render();
+
+ fireEvent.click(
+ screen.getByRole('button', { name: 'Start empty session' }),
+ );
+
+ await waitFor(() => {
+ expect(mockStartFastSession).toHaveBeenCalledWith({
+ text: '',
+ conversationId: expect.any(String),
+ empty: true,
+ model: undefined,
+ });
+ });
+ expect(mockPush).toHaveBeenCalledWith('/sessions/fast-session-1');
+ });
+
it('starts a Fast session with an image-only prompt', async () => {
mockPreparePromptAttachments.mockResolvedValueOnce({
text: '',
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.client.test.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.client.test.tsx
index a964886c7..8df2d8032 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.client.test.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.client.test.tsx
@@ -795,6 +795,32 @@ describe('FastSessionTranscript', () => {
expect(screen.getByText('Thinking')).toBeInTheDocument();
});
+ it('renders an empty persisted Session idle and accepts its first message', async () => {
+ replyMutate.mockResolvedValue({ success: true });
+ render(
+ ,
+ );
+
+ expect(screen.queryByText('Thinking')).not.toBeInTheDocument();
+ const input = screen.getByPlaceholderText('Message agent');
+ fireEvent.change(input, { target: { value: 'First message' } });
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 });
+
+ await waitFor(() =>
+ expect(replyMutate).toHaveBeenCalledWith({
+ sessionId: 'session-1',
+ text: 'First message',
+ model: null,
+ reasoningEffort: null,
+ }),
+ );
+ });
+
it('waits for the first visible assistant message before showing timeline extras', () => {
render(
(null);
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx
index 28038d750..54c91113a 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx
@@ -250,6 +250,63 @@ describe('Session detail page', () => {
);
});
+ it('renders a manually created empty Session without a pending kickoff', async () => {
+ authorizeMock.mockResolvedValue({
+ success: true,
+ userId: 'user-1',
+ isAdmin: false,
+ });
+ getSessionByIdCommandMock.mockResolvedValue({
+ id: '6a1f8f1e-0000-4000-8000-000000000004',
+ title: 'New session',
+ ownerName: 'User',
+ ownerEmail: 'user@example.com',
+ ownerImageUrl: null,
+ sourceSurface: 'web',
+ sourceTrigger: 'manual',
+ fastConversationId: '6a1f8f1e-0000-4000-8000-000000000005',
+ directInferenceCostMicroUsd: 0,
+ inferenceCostMicroUsd: 0,
+ createdAt: new Date('2026-01-01T00:00:00.000Z'),
+ status: 'ready',
+ tasks: [],
+ artifacts: [],
+ });
+ getFastSessionByIdMock.mockResolvedValue({
+ id: '6a1f8f1e-0000-4000-8000-000000000005',
+ userId: 'user-1',
+ ownerName: 'User',
+ ownerEmail: 'user@example.com',
+ ownerImageUrl: null,
+ title: null,
+ surface: 'web',
+ model: null,
+ reasoningEffort: null,
+ inferenceCostMicroUsd: 0,
+ createdAt: new Date('2026-01-01T00:00:00.000Z'),
+ messages: [],
+ hasOlderMessages: false,
+ });
+
+ renderToStaticMarkup(
+ await SessionDetailPage({
+ params: Promise.resolve({
+ sessionId: '6a1f8f1e-0000-4000-8000-000000000004',
+ }),
+ }),
+ );
+
+ expect(transcriptMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ sessionId: '6a1f8f1e-0000-4000-8000-000000000005',
+ initialMessages: [],
+ initialResponsePending: false,
+ canReply: true,
+ }),
+ undefined,
+ );
+ });
+
it('hydrates a direct Session route without seeding the response lease', async () => {
authorizeMock.mockResolvedValue({
success: true,
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
index d03a7f5b1..206ac0a00 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
@@ -158,6 +158,9 @@ export default async function SessionDetailPage({
hasOlderMessages={session.hasOlderMessages}
canReply
initialTitle={unifiedSession.title}
+ initialResponsePending={
+ unifiedSession.sourceTrigger !== 'manual'
+ }
fallbackTitle={unifiedSession.title}
sessionModel={session.model}
sessionReasoningEffort={session.reasoningEffort}
diff --git a/apps/web/src/components/tasks/NewTaskDialog.client.test.tsx b/apps/web/src/components/tasks/NewTaskDialog.client.test.tsx
index e276a0f14..30364d4fc 100644
--- a/apps/web/src/components/tasks/NewTaskDialog.client.test.tsx
+++ b/apps/web/src/components/tasks/NewTaskDialog.client.test.tsx
@@ -7,14 +7,17 @@ const { onOpenChangeMock } = vi.hoisted(() => ({
vi.mock('./NewTaskForm', () => ({
NewTaskForm: ({
animate,
+ allowEmptySession,
onTaskStarted,
}: {
animate?: boolean;
+ allowEmptySession?: boolean;
onTaskStarted: () => void;
}) => (