Skip to content
Open
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
23 changes: 21 additions & 2 deletions apps/web/src/app/(authenticated)/home/Home.client.test.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,11 @@ type PendingResponseState = {
};

type PendingResponseAction =
| { type: 'hydrate'; messages: TranscriptMessage[] }
| {
type: 'hydrate';
messages: TranscriptMessage[];
initialResponsePending?: boolean;
}
| {
type: 'messages';
messages: TranscriptMessage[];
Expand Down Expand Up @@ -149,7 +153,8 @@ export function pendingResponseReducer(
if (action.type === 'hydrate' || action.type === 'messages') {
let pendingAfter =
action.type === 'hydrate'
? action.messages.length === 0
? action.initialResponsePending !== false &&
action.messages.length === 0
? { id: '', ts: 0, turnSeq: -1 }
: null
: state.pendingAfter;
Expand Down Expand Up @@ -278,6 +283,7 @@ export function FastSessionTranscript({
hasOlderMessages,
canReply,
initialTitle = null,
initialResponsePending = true,
fallbackTitle = 'New session',
sessionModel = null,
sessionReasoningEffort = null,
Expand All @@ -292,6 +298,8 @@ export function FastSessionTranscript({
hasOlderMessages?: boolean;
canReply?: boolean;
initialTitle?: string | null;
/** Whether an empty initial transcript is waiting on a scheduled kickoff. */
initialResponsePending?: boolean;
fallbackTitle?: string;
sessionModel?: string | null;
sessionReasoningEffort?: ReasoningEffort | null;
Expand Down Expand Up @@ -333,7 +341,7 @@ export function FastSessionTranscript({
latestVisibleResponse: null,
optimisticRollback: null,
},
{ type: 'hydrate', messages },
{ type: 'hydrate', messages, initialResponsePending },
),
);
const [replyError, setReplyError] = useState<string | null>(null);
Expand Down
57 changes: 57 additions & 0 deletions apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
21 changes: 17 additions & 4 deletions apps/web/src/components/tasks/NewTaskForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useState, useCallback, useEffect, type Ref } from 'react';
import { useState, useCallback, useEffect, useRef, type Ref } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { toast } from 'sonner';

Expand Down Expand Up @@ -62,6 +62,7 @@ export function NewTaskForm({
const [selectedReasoningEffort, setSelectedReasoningEffort] = useState<
ReasoningEffort | null | undefined
>(undefined);
const emptySessionConversationIdRef = useRef<string | null>(null);

useEffect(() => setPromptText(promptParam), [promptParam]);
useEffect(() => setSelectedModelOverrideId(modelParam), [modelParam]);
Expand All @@ -75,6 +76,8 @@ export function NewTaskForm({
attachmentTexts?: string[];
model?: string | null;
reasoningEffort?: ReasoningEffort | null;
conversationId?: string;
empty?: true;
}): Promise<void> => {
// A second submit while the first is in flight would mint a second
// session and orphan one of them.
Expand Down Expand Up @@ -180,11 +183,21 @@ export function NewTaskForm({
};

if (!environmentIdParam) {
if (
const isEmpty =
!submission.description &&
!submission.images?.length &&
!submission.attachmentTexts?.length
) {
!submission.attachmentTexts?.length;
if (isEmpty) {
emptySessionConversationIdRef.current ??= crypto.randomUUID();
await startFastSession({
text: '',
conversationId: emptySessionConversationIdRef.current,
empty: true,
model: selectedModelOverrideId,
...(selectedReasoningEffort !== undefined
? { reasoningEffort: selectedReasoningEffort }
: {}),
});
return;
}
await startFastSession({
Expand Down
21 changes: 19 additions & 2 deletions apps/web/src/components/tasks/TaskPromptInput.client.test.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions apps/web/src/hooks/task-runs/useStartFastSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ type StartFastSessionVariables = {
attachmentTexts?: string[];
model?: string | null;
reasoningEffort?: ReasoningEffort | null;
/** Stable identity used to make a start retry idempotent. */
conversationId?: string;
/** Persist the Session without scheduling an initial Fast turn. */
empty?: true;
/** Launch into a chosen workspace without a Fast decision. */
pinnedLaunch?: {
launchId: string;
Expand Down
36 changes: 36 additions & 0 deletions apps/web/src/trpc/commands/fast-sessions/index.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions apps/web/src/trpc/commands/fast-sessions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ export async function startFastSessionCommand(
model?: string | null;
reasoningEffort?: ReasoningEffort | null;
conversationId?: string;
empty?: true;
pinnedLaunch?: PinnedFastSessionLaunchInput;
},
): Promise<{
Expand Down Expand Up @@ -414,6 +415,17 @@ export async function startFastSessionCommand(
});
const unifiedSession = await ensureSessionForFastConversation(db, session.id);

if (input.empty) {
await db
.update(sessions)
.set({ sourceTrigger: 'manual' })
.where(eq(sessions.id, unifiedSession.id));
return {
sessionId: unifiedSession.id,
fastConversationId: session.id,
};
}

const kickoffTurnId = input.conversationId
? `web-kickoff:${session.id}`
: undefined;
Expand Down
Loading
Loading