@@ -46,6 +46,8 @@ import { ANALYTICS_EVENTS } from "@/types/analytics";
4646
4747const log = logger . scope ( "session-service" ) ;
4848
49+ export const PREVIEW_TASK_ID = "__preview__" ;
50+
4951interface AuthCredentials {
5052 apiKey : string ;
5153 apiHost : string ;
@@ -60,6 +62,7 @@ interface ConnectParams {
6062 executionMode ?: ExecutionMode ;
6163 adapter ?: "claude" | "codex" ;
6264 model ?: string ;
65+ reasoningLevel ?: string ;
6366}
6467
6568// --- Singleton Service Instance ---
@@ -95,6 +98,8 @@ export class SessionService {
9598 permission ?: { unsubscribe : ( ) => void } ;
9699 }
97100 > ( ) ;
101+ /** Version counter to discard stale preview session results */
102+ private previewVersion = 0 ;
98103
99104 /**
100105 * Connect to a task session.
@@ -136,8 +141,15 @@ export class SessionService {
136141 }
137142
138143 private async doConnect ( params : ConnectParams ) : Promise < void > {
139- const { task, repoPath, initialPrompt, executionMode, adapter, model } =
140- params ;
144+ const {
145+ task,
146+ repoPath,
147+ initialPrompt,
148+ executionMode,
149+ adapter,
150+ model,
151+ reasoningLevel,
152+ } = params ;
141153 const { id : taskId , latest_run : latestRun } = task ;
142154 const taskTitle = task . title || task . description || "Task" ;
143155
@@ -214,6 +226,7 @@ export class SessionService {
214226 executionMode ,
215227 adapter ,
216228 model ,
229+ reasoningLevel ,
217230 ) ;
218231 }
219232 } catch ( error ) {
@@ -360,6 +373,7 @@ export class SessionService {
360373 executionMode ?: ExecutionMode ,
361374 adapter ?: "claude" | "codex" ,
362375 model ?: string ,
376+ reasoningLevel ?: string ,
363377 ) : Promise < void > {
364378 if ( ! auth . client ) {
365379 throw new Error ( "Unable to reach server. Please check your connection." ) ;
@@ -419,6 +433,15 @@ export class SessionService {
419433 ) ;
420434 }
421435
436+ // Set reasoning level if provided (e.g., from Codex adapter's preview session)
437+ if ( reasoningLevel ) {
438+ await this . setSessionConfigOptionByCategory (
439+ taskId ,
440+ "thought_level" ,
441+ reasoningLevel ,
442+ ) ;
443+ }
444+
422445 if ( initialPrompt ?. length ) {
423446 await this . sendPrompt ( taskId , initialPrompt ) ;
424447 }
@@ -442,6 +465,127 @@ export class SessionService {
442465 sessionStoreSetters . removeSession ( session . taskRunId ) ;
443466 }
444467
468+ // --- Preview Session Management ---
469+
470+ /**
471+ * Start a lightweight preview session for the task input page.
472+ * This session is used solely to retrieve adapter-specific config options
473+ * (models, modes, reasoning levels) without creating a real PostHog task.
474+ *
475+ * Uses a version counter to prevent race conditions when rapidly switching
476+ * adapters — stale results from a previous start are discarded.
477+ */
478+ async startPreviewSession ( params : {
479+ adapter : "claude" | "codex" ;
480+ repoPath ?: string ;
481+ } ) : Promise < void > {
482+ // Increment version to invalidate any in-flight start
483+ const version = ++ this . previewVersion ;
484+
485+ // Cancel any existing preview session first
486+ await this . cancelPreviewSession ( ) ;
487+
488+ // Check if a newer start was requested while we were cancelling
489+ if ( version !== this . previewVersion ) {
490+ log . info ( "Preview session start superseded, skipping" , { version } ) ;
491+ return ;
492+ }
493+
494+ const auth = this . getAuthCredentials ( ) ;
495+ if ( ! auth ) {
496+ log . info ( "Skipping preview session - not authenticated" ) ;
497+ return ;
498+ }
499+
500+ const taskRunId = `preview-${ crypto . randomUUID ( ) } ` ;
501+ const session = this . createBaseSession (
502+ taskRunId ,
503+ PREVIEW_TASK_ID ,
504+ "Preview" ,
505+ ) ;
506+ session . adapter = params . adapter ;
507+ sessionStoreSetters . setSession ( session ) ;
508+
509+ try {
510+ const result = await trpcVanilla . agent . start . mutate ( {
511+ taskId : PREVIEW_TASK_ID ,
512+ taskRunId,
513+ repoPath : params . repoPath || "~" ,
514+ apiKey : auth . apiKey ,
515+ apiHost : auth . apiHost ,
516+ projectId : auth . projectId ,
517+ adapter : params . adapter ,
518+ } ) ;
519+
520+ // Check again after the async start — a newer call may have superseded us
521+ if ( version !== this . previewVersion ) {
522+ log . info (
523+ "Preview session start superseded after agent.start, cleaning up stale session" ,
524+ { taskRunId, version } ,
525+ ) ;
526+ // Clean up the session we just started but is now stale
527+ trpcVanilla . agent . cancel
528+ . mutate ( { sessionId : taskRunId } )
529+ . catch ( ( err ) => {
530+ log . warn ( "Failed to cancel stale preview session" , {
531+ taskRunId,
532+ error : err ,
533+ } ) ;
534+ } ) ;
535+ sessionStoreSetters . removeSession ( taskRunId ) ;
536+ return ;
537+ }
538+
539+ const configOptions = result . configOptions as
540+ | SessionConfigOption [ ]
541+ | undefined ;
542+
543+ sessionStoreSetters . updateSession ( taskRunId , {
544+ status : "connected" ,
545+ channel : result . channel ,
546+ configOptions,
547+ } ) ;
548+
549+ this . subscribeToChannel ( taskRunId ) ;
550+
551+ log . info ( "Preview session started" , {
552+ taskRunId,
553+ adapter : params . adapter ,
554+ configOptionsCount : configOptions ?. length ?? 0 ,
555+ } ) ;
556+ } catch ( error ) {
557+ // Only clean up if we're still the current version
558+ if ( version === this . previewVersion ) {
559+ log . error ( "Failed to start preview session" , { error } ) ;
560+ sessionStoreSetters . removeSession ( taskRunId ) ;
561+ }
562+ }
563+ }
564+
565+ /**
566+ * Cancel and clean up the preview session.
567+ * Unsubscribes and removes from store first (so nothing writes to the old
568+ * session), then awaits the cancel on the main process.
569+ */
570+ async cancelPreviewSession ( ) : Promise < void > {
571+ const session = sessionStoreSetters . getSessionByTaskId ( PREVIEW_TASK_ID ) ;
572+ if ( ! session ) return ;
573+
574+ const { taskRunId } = session ;
575+
576+ // Unsubscribe and remove from store first so nothing writes to the old session
577+ this . unsubscribeFromChannel ( taskRunId ) ;
578+ sessionStoreSetters . removeSession ( taskRunId ) ;
579+
580+ try {
581+ await trpcVanilla . agent . cancel . mutate ( { sessionId : taskRunId } ) ;
582+ } catch ( error ) {
583+ log . warn ( "Failed to cancel preview session" , { taskRunId, error } ) ;
584+ }
585+
586+ log . info ( "Preview session cancelled" , { taskRunId } ) ;
587+ }
588+
445589 // --- Subscription Management ---
446590
447591 private subscribeToChannel ( taskRunId : string ) : void {
@@ -511,6 +655,7 @@ export class SessionService {
511655 }
512656
513657 this . connectingTasks . clear ( ) ;
658+ this . previewVersion = 0 ;
514659 }
515660
516661 private handleSessionEvent ( taskRunId : string , acpMsg : AcpMessage ) : void {
0 commit comments