@@ -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 ) {
@@ -366,6 +379,7 @@ export class SessionService {
366379 executionMode ?: ExecutionMode ,
367380 adapter ?: "claude" | "codex" ,
368381 model ?: string ,
382+ reasoningLevel ?: string ,
369383 ) : Promise < void > {
370384 if ( ! auth . client ) {
371385 throw new Error ( "Unable to reach server. Please check your connection." ) ;
@@ -425,6 +439,15 @@ export class SessionService {
425439 ) ;
426440 }
427441
442+ // Set reasoning level if provided (e.g., from Codex adapter's preview session)
443+ if ( reasoningLevel ) {
444+ await this . setSessionConfigOptionByCategory (
445+ taskId ,
446+ "thought_level" ,
447+ reasoningLevel ,
448+ ) ;
449+ }
450+
428451 if ( initialPrompt ?. length ) {
429452 await this . sendPrompt ( taskId , initialPrompt ) ;
430453 }
@@ -448,6 +471,127 @@ export class SessionService {
448471 sessionStoreSetters . removeSession ( session . taskRunId ) ;
449472 }
450473
474+ // --- Preview Session Management ---
475+
476+ /**
477+ * Start a lightweight preview session for the task input page.
478+ * This session is used solely to retrieve adapter-specific config options
479+ * (models, modes, reasoning levels) without creating a real PostHog task.
480+ *
481+ * Uses a version counter to prevent race conditions when rapidly switching
482+ * adapters — stale results from a previous start are discarded.
483+ */
484+ async startPreviewSession ( params : {
485+ adapter : "claude" | "codex" ;
486+ repoPath ?: string ;
487+ } ) : Promise < void > {
488+ // Increment version to invalidate any in-flight start
489+ const version = ++ this . previewVersion ;
490+
491+ // Cancel any existing preview session first
492+ await this . cancelPreviewSession ( ) ;
493+
494+ // Check if a newer start was requested while we were cancelling
495+ if ( version !== this . previewVersion ) {
496+ log . info ( "Preview session start superseded, skipping" , { version } ) ;
497+ return ;
498+ }
499+
500+ const auth = this . getAuthCredentials ( ) ;
501+ if ( ! auth ) {
502+ log . info ( "Skipping preview session - not authenticated" ) ;
503+ return ;
504+ }
505+
506+ const taskRunId = `preview-${ crypto . randomUUID ( ) } ` ;
507+ const session = this . createBaseSession (
508+ taskRunId ,
509+ PREVIEW_TASK_ID ,
510+ "Preview" ,
511+ ) ;
512+ session . adapter = params . adapter ;
513+ sessionStoreSetters . setSession ( session ) ;
514+
515+ try {
516+ const result = await trpcVanilla . agent . start . mutate ( {
517+ taskId : PREVIEW_TASK_ID ,
518+ taskRunId,
519+ repoPath : params . repoPath || "~" ,
520+ apiKey : auth . apiKey ,
521+ apiHost : auth . apiHost ,
522+ projectId : auth . projectId ,
523+ adapter : params . adapter ,
524+ } ) ;
525+
526+ // Check again after the async start — a newer call may have superseded us
527+ if ( version !== this . previewVersion ) {
528+ log . info (
529+ "Preview session start superseded after agent.start, cleaning up stale session" ,
530+ { taskRunId, version } ,
531+ ) ;
532+ // Clean up the session we just started but is now stale
533+ trpcVanilla . agent . cancel
534+ . mutate ( { sessionId : taskRunId } )
535+ . catch ( ( err ) => {
536+ log . warn ( "Failed to cancel stale preview session" , {
537+ taskRunId,
538+ error : err ,
539+ } ) ;
540+ } ) ;
541+ sessionStoreSetters . removeSession ( taskRunId ) ;
542+ return ;
543+ }
544+
545+ const configOptions = result . configOptions as
546+ | SessionConfigOption [ ]
547+ | undefined ;
548+
549+ sessionStoreSetters . updateSession ( taskRunId , {
550+ status : "connected" ,
551+ channel : result . channel ,
552+ configOptions,
553+ } ) ;
554+
555+ this . subscribeToChannel ( taskRunId ) ;
556+
557+ log . info ( "Preview session started" , {
558+ taskRunId,
559+ adapter : params . adapter ,
560+ configOptionsCount : configOptions ?. length ?? 0 ,
561+ } ) ;
562+ } catch ( error ) {
563+ // Only clean up if we're still the current version
564+ if ( version === this . previewVersion ) {
565+ log . error ( "Failed to start preview session" , { error } ) ;
566+ sessionStoreSetters . removeSession ( taskRunId ) ;
567+ }
568+ }
569+ }
570+
571+ /**
572+ * Cancel and clean up the preview session.
573+ * Unsubscribes and removes from store first (so nothing writes to the old
574+ * session), then awaits the cancel on the main process.
575+ */
576+ async cancelPreviewSession ( ) : Promise < void > {
577+ const session = sessionStoreSetters . getSessionByTaskId ( PREVIEW_TASK_ID ) ;
578+ if ( ! session ) return ;
579+
580+ const { taskRunId } = session ;
581+
582+ // Unsubscribe and remove from store first so nothing writes to the old session
583+ this . unsubscribeFromChannel ( taskRunId ) ;
584+ sessionStoreSetters . removeSession ( taskRunId ) ;
585+
586+ try {
587+ await trpcVanilla . agent . cancel . mutate ( { sessionId : taskRunId } ) ;
588+ } catch ( error ) {
589+ log . warn ( "Failed to cancel preview session" , { taskRunId, error } ) ;
590+ }
591+
592+ log . info ( "Preview session cancelled" , { taskRunId } ) ;
593+ }
594+
451595 // --- Subscription Management ---
452596
453597 private subscribeToChannel ( taskRunId : string ) : void {
@@ -517,6 +661,7 @@ export class SessionService {
517661 }
518662
519663 this . connectingTasks . clear ( ) ;
664+ this . previewVersion = 0 ;
520665 }
521666
522667 private handleSessionEvent ( taskRunId : string , acpMsg : AcpMessage ) : void {
0 commit comments