@@ -403,39 +403,96 @@ export class WorkspaceService extends EventEmitter {
403403 }
404404 }
405405
406- async createForFirstMessage (
406+ createForFirstMessage (
407407 message : string ,
408408 projectPath : string ,
409409 options : SendMessageOptions & {
410410 imageParts ?: Array < { url : string ; mediaType : string } > ;
411411 runtimeConfig ?: RuntimeConfig ;
412412 trunkBranch ?: string ;
413413 } = { model : "claude-3-5-sonnet-20241022" }
414- ) : Promise <
414+ ) :
415415 | { success : true ; workspaceId : string ; metadata : FrontendWorkspaceMetadata }
416- | { success : false ; error : string }
417- > {
418- try {
419- // Use placeholder name for immediate workspace creation (non-blocking)
420- const placeholderName = generatePlaceholderName ( message ) ;
421- log . debug ( "Using placeholder name for immediate creation" , { placeholderName } ) ;
416+ | { success : false ; error : string } {
417+ // Generate placeholder name and ID immediately (non-blocking)
418+ const placeholderName = generatePlaceholderName ( message ) ;
419+ const workspaceId = this . config . generateStableId ( ) ;
420+ const projectName = projectPath . split ( "/" ) . pop ( ) ?? projectPath . split ( "\\" ) . pop ( ) ?? "unknown" ;
422421
423- const branches = await listLocalBranches ( projectPath ) ;
424- const recommendedTrunk =
425- options . trunkBranch ?? ( await detectDefaultTrunkBranch ( projectPath , branches ) ) ?? "main" ;
422+ // Use provided runtime config or default to worktree
423+ const runtimeConfig : RuntimeConfig = options . runtimeConfig ?? {
424+ type : "worktree" ,
425+ srcBaseDir : this . config . srcDir ,
426+ } ;
426427
427- // Default to worktree runtime for backward compatibility
428- let finalRuntimeConfig : RuntimeConfig = options . runtimeConfig ?? {
429- type : "worktree" ,
430- srcBaseDir : this . config . srcDir ,
431- } ;
428+ // Compute preliminary workspace path (may be refined after srcBaseDir resolution)
429+ const srcBaseDir = getSrcBaseDir ( runtimeConfig ) ?? this . config . srcDir ;
430+ const preliminaryWorkspacePath = path . join ( srcBaseDir , projectName , placeholderName ) ;
431+
432+ // Create preliminary metadata with "creating" status for immediate UI response
433+ const preliminaryMetadata : FrontendWorkspaceMetadata = {
434+ id : workspaceId ,
435+ name : placeholderName ,
436+ projectName,
437+ projectPath,
438+ createdAt : new Date ( ) . toISOString ( ) ,
439+ namedWorkspacePath : preliminaryWorkspacePath ,
440+ runtimeConfig,
441+ status : "creating" ,
442+ } ;
443+
444+ // Create session and emit metadata immediately so frontend can switch
445+ const session = this . getOrCreateSession ( workspaceId ) ;
446+ session . emitMetadata ( preliminaryMetadata ) ;
447+
448+ log . debug ( "Emitted preliminary workspace metadata" , { workspaceId, placeholderName } ) ;
449+
450+ // Kick off background workspace creation (git operations, config save, etc.)
451+ void this . completeWorkspaceCreation (
452+ workspaceId ,
453+ message ,
454+ projectPath ,
455+ placeholderName ,
456+ runtimeConfig ,
457+ options
458+ ) ;
459+
460+ // Return immediately with preliminary metadata
461+ return {
462+ success : true ,
463+ workspaceId,
464+ metadata : preliminaryMetadata ,
465+ } ;
466+ }
432467
433- const workspaceId = this . config . generateStableId ( ) ;
468+ /**
469+ * Completes workspace creation in the background after preliminary metadata is emitted.
470+ * Handles git operations, config persistence, and kicks off message sending.
471+ */
472+ private async completeWorkspaceCreation (
473+ workspaceId : string ,
474+ message : string ,
475+ projectPath : string ,
476+ placeholderName : string ,
477+ runtimeConfig : RuntimeConfig ,
478+ options : SendMessageOptions & {
479+ imageParts ?: Array < { url : string ; mediaType : string } > ;
480+ runtimeConfig ?: RuntimeConfig ;
481+ trunkBranch ?: string ;
482+ }
483+ ) : Promise < void > {
484+ const session = this . sessions . get ( workspaceId ) ;
485+ if ( ! session ) {
486+ log . error ( "Session not found for workspace creation" , { workspaceId } ) ;
487+ return ;
488+ }
434489
490+ try {
491+ // Resolve runtime config (may involve path resolution for SSH)
492+ let finalRuntimeConfig = runtimeConfig ;
435493 let runtime ;
436494 try {
437495 runtime = createRuntime ( finalRuntimeConfig , { projectPath } ) ;
438- // Resolve srcBaseDir path if the config has one
439496 const srcBaseDir = getSrcBaseDir ( finalRuntimeConfig ) ;
440497 if ( srcBaseDir ) {
441498 const resolvedSrcBaseDir = await runtime . resolvePath ( srcBaseDir ) ;
@@ -449,10 +506,16 @@ export class WorkspaceService extends EventEmitter {
449506 }
450507 } catch ( error ) {
451508 const errorMsg = error instanceof Error ? error . message : String ( error ) ;
452- return { success : false , error : errorMsg } ;
509+ log . error ( "Failed to create runtime for workspace" , { workspaceId, error : errorMsg } ) ;
510+ session . emitMetadata ( null ) ; // Remove the "creating" workspace
511+ return ;
453512 }
454513
455- const session = this . getOrCreateSession ( workspaceId ) ;
514+ // Detect trunk branch (git operation)
515+ const branches = await listLocalBranches ( projectPath ) ;
516+ const recommendedTrunk =
517+ options . trunkBranch ?? ( await detectDefaultTrunkBranch ( projectPath , branches ) ) ?? "main" ;
518+
456519 this . initStateManager . startInit ( workspaceId , projectPath ) ;
457520 const initLogger = this . createInitLogger ( workspaceId ) ;
458521
@@ -471,7 +534,6 @@ export class WorkspaceService extends EventEmitter {
471534
472535 if ( createResult . success ) break ;
473536
474- // If collision and not last attempt, retry with suffix
475537 if (
476538 isWorkspaceNameCollision ( createResult . error ) &&
477539 attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES
@@ -484,25 +546,20 @@ export class WorkspaceService extends EventEmitter {
484546 }
485547
486548 if ( ! createResult ! . success || ! createResult ! . workspacePath ) {
487- return { success : false , error : createResult ! . error ?? "Failed to create workspace" } ;
549+ log . error ( "Failed to create workspace" , {
550+ workspaceId,
551+ error : createResult ! . error ,
552+ } ) ;
553+ session . emitMetadata ( null ) ; // Remove the "creating" workspace
554+ return ;
488555 }
489556
490557 const projectName =
491558 projectPath . split ( "/" ) . pop ( ) ?? projectPath . split ( "\\" ) . pop ( ) ?? "unknown" ;
492-
493- // Compute namedWorkspacePath
494559 const namedWorkspacePath = runtime . getWorkspacePath ( projectPath , finalBranchName ) ;
560+ const createdAt = new Date ( ) . toISOString ( ) ;
495561
496- const metadata : FrontendWorkspaceMetadata = {
497- id : workspaceId ,
498- name : finalBranchName ,
499- projectName,
500- projectPath,
501- createdAt : new Date ( ) . toISOString ( ) ,
502- namedWorkspacePath,
503- runtimeConfig : finalRuntimeConfig ,
504- } ;
505-
562+ // Save to config
506563 await this . config . editConfig ( ( config ) => {
507564 let projectConfig = config . projects . get ( projectPath ) ;
508565 if ( ! projectConfig ) {
@@ -513,21 +570,27 @@ export class WorkspaceService extends EventEmitter {
513570 path : createResult ! . workspacePath ! ,
514571 id : workspaceId ,
515572 name : finalBranchName ,
516- createdAt : metadata . createdAt ,
573+ createdAt,
517574 runtimeConfig : finalRuntimeConfig ,
518575 } ) ;
519576 return config ;
520577 } ) ;
521578
522- const allMetadata = await this . config . getAllWorkspaceMetadata ( ) ;
523- const completeMetadata = allMetadata . find ( ( m ) => m . id === workspaceId ) ;
524-
525- if ( ! completeMetadata ) {
526- return { success : false , error : "Failed to retrieve workspace metadata" } ;
527- }
579+ // Emit final metadata (without "creating" status)
580+ const finalMetadata : FrontendWorkspaceMetadata = {
581+ id : workspaceId ,
582+ name : finalBranchName ,
583+ projectName,
584+ projectPath,
585+ createdAt,
586+ namedWorkspacePath,
587+ runtimeConfig : finalRuntimeConfig ,
588+ } ;
589+ session . emitMetadata ( finalMetadata ) ;
528590
529- session . emitMetadata ( completeMetadata ) ;
591+ log . debug ( "Workspace creation completed" , { workspaceId , finalBranchName } ) ;
530592
593+ // Start workspace initialization in background
531594 void runtime
532595 . initWorkspace ( {
533596 projectPath,
@@ -543,20 +606,15 @@ export class WorkspaceService extends EventEmitter {
543606 initLogger . logComplete ( - 1 ) ;
544607 } ) ;
545608
609+ // Send the first message
546610 void session . sendMessage ( message , options ) ;
547611
548612 // Generate AI name asynchronously and rename if successful
549613 void this . generateAndApplyAIName ( workspaceId , message , finalBranchName , options . model ) ;
550-
551- return {
552- success : true ,
553- workspaceId,
554- metadata : completeMetadata ,
555- } ;
556614 } catch ( error ) {
557615 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
558- log . error ( "Unexpected error in createWorkspaceForFirstMessage: " , error ) ;
559- return { success : false , error : `Failed to create workspace: ${ errorMessage } ` } ;
616+ log . error ( "Unexpected error in workspace creation " , { workspaceId , error : errorMessage } ) ;
617+ session . emitMetadata ( null ) ; // Remove the "creating" workspace
560618 }
561619 }
562620
@@ -613,24 +671,38 @@ export class WorkspaceService extends EventEmitter {
613671 // Wait for the stream to complete before renaming (rename is blocked during streaming)
614672 await this . waitForStreamComplete ( workspaceId ) ;
615673
616- // Attempt to rename the workspace
617- const renameResult = await this . rename ( workspaceId , aiGeneratedName ) ;
674+ // Attempt to rename with collision retry (same logic as workspace creation)
675+ let finalName = aiGeneratedName ;
676+ for ( let attempt = 0 ; attempt <= MAX_WORKSPACE_NAME_COLLISION_RETRIES ; attempt ++ ) {
677+ const renameResult = await this . rename ( workspaceId , finalName ) ;
678+
679+ if ( renameResult . success ) {
680+ log . info ( "Successfully renamed workspace to AI-generated name" , {
681+ workspaceId,
682+ oldName : currentName ,
683+ newName : finalName ,
684+ } ) ;
685+ return ;
686+ }
618687
619- if ( ! renameResult . success ) {
620- // Rename failed (e.g., collision) - keep the placeholder name
688+ // If collision and not last attempt, retry with suffix
689+ if (
690+ renameResult . error ?. includes ( "already exists" ) &&
691+ attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES
692+ ) {
693+ log . debug ( `Workspace name collision for "${ finalName } ", retrying with suffix` ) ;
694+ finalName = appendCollisionSuffix ( aiGeneratedName ) ;
695+ continue ;
696+ }
697+
698+ // Non-collision error or out of retries - keep placeholder name
621699 log . info ( "Failed to rename workspace to AI-generated name" , {
622700 workspaceId,
623- aiGeneratedName,
701+ aiGeneratedName : finalName ,
624702 error : renameResult . error ,
625703 } ) ;
626704 return ;
627705 }
628-
629- log . info ( "Successfully renamed workspace to AI-generated name" , {
630- workspaceId,
631- oldName : currentName ,
632- newName : aiGeneratedName ,
633- } ) ;
634706 } catch ( error ) {
635707 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
636708 log . error ( "Unexpected error in async AI name generation" , {
@@ -1002,7 +1074,7 @@ export class WorkspaceService extends EventEmitter {
10021074 messagePreview : message . substring ( 0 , 50 ) ,
10031075 } ) ;
10041076
1005- return await this . createForFirstMessage ( message , options . projectPath , options ) ;
1077+ return this . createForFirstMessage ( message , options . projectPath , options ) ;
10061078 }
10071079
10081080 log . debug ( "sendMessage handler: Received" , {
0 commit comments