@@ -402,39 +402,96 @@ export class WorkspaceService extends EventEmitter {
402402 }
403403 }
404404
405- async createForFirstMessage (
405+ createForFirstMessage (
406406 message : string ,
407407 projectPath : string ,
408408 options : SendMessageOptions & {
409409 imageParts ?: Array < { url : string ; mediaType : string } > ;
410410 runtimeConfig ?: RuntimeConfig ;
411411 trunkBranch ?: string ;
412412 } = { model : "claude-3-5-sonnet-20241022" }
413- ) : Promise <
413+ ) :
414414 | { success : true ; workspaceId : string ; metadata : FrontendWorkspaceMetadata }
415- | { success : false ; error : string }
416- > {
417- try {
418- // Use placeholder name for immediate workspace creation (non-blocking)
419- const placeholderName = generatePlaceholderName ( message ) ;
420- log . debug ( "Using placeholder name for immediate creation" , { placeholderName } ) ;
415+ | { success : false ; error : string } {
416+ // Generate placeholder name and ID immediately (non-blocking)
417+ const placeholderName = generatePlaceholderName ( message ) ;
418+ const workspaceId = this . config . generateStableId ( ) ;
419+ const projectName = projectPath . split ( "/" ) . pop ( ) ?? projectPath . split ( "\\" ) . pop ( ) ?? "unknown" ;
421420
422- const branches = await listLocalBranches ( projectPath ) ;
423- const recommendedTrunk =
424- options . trunkBranch ?? ( await detectDefaultTrunkBranch ( projectPath , branches ) ) ?? "main" ;
421+ // Use provided runtime config or default to worktree
422+ const runtimeConfig : RuntimeConfig = options . runtimeConfig ?? {
423+ type : "worktree" ,
424+ srcBaseDir : this . config . srcDir ,
425+ } ;
425426
426- // Default to worktree runtime for backward compatibility
427- let finalRuntimeConfig : RuntimeConfig = options . runtimeConfig ?? {
428- type : "worktree" ,
429- srcBaseDir : this . config . srcDir ,
430- } ;
427+ // Compute preliminary workspace path (may be refined after srcBaseDir resolution)
428+ const srcBaseDir = getSrcBaseDir ( runtimeConfig ) ?? this . config . srcDir ;
429+ const preliminaryWorkspacePath = path . join ( srcBaseDir , projectName , placeholderName ) ;
430+
431+ // Create preliminary metadata with "creating" status for immediate UI response
432+ const preliminaryMetadata : FrontendWorkspaceMetadata = {
433+ id : workspaceId ,
434+ name : placeholderName ,
435+ projectName,
436+ projectPath,
437+ createdAt : new Date ( ) . toISOString ( ) ,
438+ namedWorkspacePath : preliminaryWorkspacePath ,
439+ runtimeConfig,
440+ status : "creating" ,
441+ } ;
442+
443+ // Create session and emit metadata immediately so frontend can switch
444+ const session = this . getOrCreateSession ( workspaceId ) ;
445+ session . emitMetadata ( preliminaryMetadata ) ;
446+
447+ log . debug ( "Emitted preliminary workspace metadata" , { workspaceId, placeholderName } ) ;
448+
449+ // Kick off background workspace creation (git operations, config save, etc.)
450+ void this . completeWorkspaceCreation (
451+ workspaceId ,
452+ message ,
453+ projectPath ,
454+ placeholderName ,
455+ runtimeConfig ,
456+ options
457+ ) ;
458+
459+ // Return immediately with preliminary metadata
460+ return {
461+ success : true ,
462+ workspaceId,
463+ metadata : preliminaryMetadata ,
464+ } ;
465+ }
431466
432- const workspaceId = this . config . generateStableId ( ) ;
467+ /**
468+ * Completes workspace creation in the background after preliminary metadata is emitted.
469+ * Handles git operations, config persistence, and kicks off message sending.
470+ */
471+ private async completeWorkspaceCreation (
472+ workspaceId : string ,
473+ message : string ,
474+ projectPath : string ,
475+ placeholderName : string ,
476+ runtimeConfig : RuntimeConfig ,
477+ options : SendMessageOptions & {
478+ imageParts ?: Array < { url : string ; mediaType : string } > ;
479+ runtimeConfig ?: RuntimeConfig ;
480+ trunkBranch ?: string ;
481+ }
482+ ) : Promise < void > {
483+ const session = this . sessions . get ( workspaceId ) ;
484+ if ( ! session ) {
485+ log . error ( "Session not found for workspace creation" , { workspaceId } ) ;
486+ return ;
487+ }
433488
489+ try {
490+ // Resolve runtime config (may involve path resolution for SSH)
491+ let finalRuntimeConfig = runtimeConfig ;
434492 let runtime ;
435493 try {
436494 runtime = createRuntime ( finalRuntimeConfig , { projectPath } ) ;
437- // Resolve srcBaseDir path if the config has one
438495 const srcBaseDir = getSrcBaseDir ( finalRuntimeConfig ) ;
439496 if ( srcBaseDir ) {
440497 const resolvedSrcBaseDir = await runtime . resolvePath ( srcBaseDir ) ;
@@ -448,10 +505,16 @@ export class WorkspaceService extends EventEmitter {
448505 }
449506 } catch ( error ) {
450507 const errorMsg = error instanceof Error ? error . message : String ( error ) ;
451- return { success : false , error : errorMsg } ;
508+ log . error ( "Failed to create runtime for workspace" , { workspaceId, error : errorMsg } ) ;
509+ session . emitMetadata ( null ) ; // Remove the "creating" workspace
510+ return ;
452511 }
453512
454- const session = this . getOrCreateSession ( workspaceId ) ;
513+ // Detect trunk branch (git operation)
514+ const branches = await listLocalBranches ( projectPath ) ;
515+ const recommendedTrunk =
516+ options . trunkBranch ?? ( await detectDefaultTrunkBranch ( projectPath , branches ) ) ?? "main" ;
517+
455518 this . initStateManager . startInit ( workspaceId , projectPath ) ;
456519 const initLogger = this . createInitLogger ( workspaceId ) ;
457520
@@ -470,7 +533,6 @@ export class WorkspaceService extends EventEmitter {
470533
471534 if ( createResult . success ) break ;
472535
473- // If collision and not last attempt, retry with suffix
474536 if (
475537 isWorkspaceNameCollision ( createResult . error ) &&
476538 attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES
@@ -483,25 +545,20 @@ export class WorkspaceService extends EventEmitter {
483545 }
484546
485547 if ( ! createResult ! . success || ! createResult ! . workspacePath ) {
486- return { success : false , error : createResult ! . error ?? "Failed to create workspace" } ;
548+ log . error ( "Failed to create workspace" , {
549+ workspaceId,
550+ error : createResult ! . error ,
551+ } ) ;
552+ session . emitMetadata ( null ) ; // Remove the "creating" workspace
553+ return ;
487554 }
488555
489556 const projectName =
490557 projectPath . split ( "/" ) . pop ( ) ?? projectPath . split ( "\\" ) . pop ( ) ?? "unknown" ;
491-
492- // Compute namedWorkspacePath
493558 const namedWorkspacePath = runtime . getWorkspacePath ( projectPath , finalBranchName ) ;
559+ const createdAt = new Date ( ) . toISOString ( ) ;
494560
495- const metadata : FrontendWorkspaceMetadata = {
496- id : workspaceId ,
497- name : finalBranchName ,
498- projectName,
499- projectPath,
500- createdAt : new Date ( ) . toISOString ( ) ,
501- namedWorkspacePath,
502- runtimeConfig : finalRuntimeConfig ,
503- } ;
504-
561+ // Save to config
505562 await this . config . editConfig ( ( config ) => {
506563 let projectConfig = config . projects . get ( projectPath ) ;
507564 if ( ! projectConfig ) {
@@ -512,21 +569,27 @@ export class WorkspaceService extends EventEmitter {
512569 path : createResult ! . workspacePath ! ,
513570 id : workspaceId ,
514571 name : finalBranchName ,
515- createdAt : metadata . createdAt ,
572+ createdAt,
516573 runtimeConfig : finalRuntimeConfig ,
517574 } ) ;
518575 return config ;
519576 } ) ;
520577
521- const allMetadata = await this . config . getAllWorkspaceMetadata ( ) ;
522- const completeMetadata = allMetadata . find ( ( m ) => m . id === workspaceId ) ;
523-
524- if ( ! completeMetadata ) {
525- return { success : false , error : "Failed to retrieve workspace metadata" } ;
526- }
578+ // Emit final metadata (without "creating" status)
579+ const finalMetadata : FrontendWorkspaceMetadata = {
580+ id : workspaceId ,
581+ name : finalBranchName ,
582+ projectName,
583+ projectPath,
584+ createdAt,
585+ namedWorkspacePath,
586+ runtimeConfig : finalRuntimeConfig ,
587+ } ;
588+ session . emitMetadata ( finalMetadata ) ;
527589
528- session . emitMetadata ( completeMetadata ) ;
590+ log . debug ( "Workspace creation completed" , { workspaceId , finalBranchName } ) ;
529591
592+ // Start workspace initialization in background
530593 void runtime
531594 . initWorkspace ( {
532595 projectPath,
@@ -542,20 +605,15 @@ export class WorkspaceService extends EventEmitter {
542605 initLogger . logComplete ( - 1 ) ;
543606 } ) ;
544607
608+ // Send the first message
545609 void session . sendMessage ( message , options ) ;
546610
547611 // Generate AI name asynchronously and rename if successful
548612 void this . generateAndApplyAIName ( workspaceId , message , finalBranchName , options . model ) ;
549-
550- return {
551- success : true ,
552- workspaceId,
553- metadata : completeMetadata ,
554- } ;
555613 } catch ( error ) {
556614 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
557- log . error ( "Unexpected error in createWorkspaceForFirstMessage: " , error ) ;
558- return { success : false , error : `Failed to create workspace: ${ errorMessage } ` } ;
615+ log . error ( "Unexpected error in workspace creation " , { workspaceId , error : errorMessage } ) ;
616+ session . emitMetadata ( null ) ; // Remove the "creating" workspace
559617 }
560618 }
561619
@@ -612,24 +670,38 @@ export class WorkspaceService extends EventEmitter {
612670 // Wait for the stream to complete before renaming (rename is blocked during streaming)
613671 await this . waitForStreamComplete ( workspaceId ) ;
614672
615- // Attempt to rename the workspace
616- const renameResult = await this . rename ( workspaceId , aiGeneratedName ) ;
673+ // Attempt to rename with collision retry (same logic as workspace creation)
674+ let finalName = aiGeneratedName ;
675+ for ( let attempt = 0 ; attempt <= MAX_WORKSPACE_NAME_COLLISION_RETRIES ; attempt ++ ) {
676+ const renameResult = await this . rename ( workspaceId , finalName ) ;
677+
678+ if ( renameResult . success ) {
679+ log . info ( "Successfully renamed workspace to AI-generated name" , {
680+ workspaceId,
681+ oldName : currentName ,
682+ newName : finalName ,
683+ } ) ;
684+ return ;
685+ }
617686
618- if ( ! renameResult . success ) {
619- // Rename failed (e.g., collision) - keep the placeholder name
687+ // If collision and not last attempt, retry with suffix
688+ if (
689+ renameResult . error ?. includes ( "already exists" ) &&
690+ attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES
691+ ) {
692+ log . debug ( `Workspace name collision for "${ finalName } ", retrying with suffix` ) ;
693+ finalName = appendCollisionSuffix ( aiGeneratedName ) ;
694+ continue ;
695+ }
696+
697+ // Non-collision error or out of retries - keep placeholder name
620698 log . info ( "Failed to rename workspace to AI-generated name" , {
621699 workspaceId,
622- aiGeneratedName,
700+ aiGeneratedName : finalName ,
623701 error : renameResult . error ,
624702 } ) ;
625703 return ;
626704 }
627-
628- log . info ( "Successfully renamed workspace to AI-generated name" , {
629- workspaceId,
630- oldName : currentName ,
631- newName : aiGeneratedName ,
632- } ) ;
633705 } catch ( error ) {
634706 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
635707 log . error ( "Unexpected error in async AI name generation" , {
@@ -1001,7 +1073,7 @@ export class WorkspaceService extends EventEmitter {
10011073 messagePreview : message . substring ( 0 , 50 ) ,
10021074 } ) ;
10031075
1004- return await this . createForFirstMessage ( message , options . projectPath , options ) ;
1076+ return this . createForFirstMessage ( message , options . projectPath , options ) ;
10051077 }
10061078
10071079 log . debug ( "sendMessage handler: Received" , {
0 commit comments