Skip to content

Commit 2015cb2

Browse files
committed
🤖 perf: async workspace name generation for faster UX
Previously, workspace creation blocked for up to 10s while waiting for the AI to generate a workspace name. This caused a poor UX where users had to wait before seeing any result. Changes: - Use generatePlaceholderName() to create workspace immediately with a temporary name derived from the user's message (e.g., 'add-user-auth') - Generate AI name asynchronously in the background - If AI generation succeeds and differs from placeholder, rename workspace - If AI generation or rename fails, gracefully keep the placeholder name The workspace now appears instantly while the AI-generated name is applied as soon as it's available. Users see their workspace right away instead of waiting for the AI provider to respond. _Generated with `mux`_
1 parent 43c5f8a commit 2015cb2

File tree

2 files changed

+118
-20
lines changed

2 files changed

+118
-20
lines changed

src/node/services/workspaceService.ts

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type { InitStateManager } from "@/node/services/initStateManager";
1414
import type { ExtensionMetadataService } from "@/node/services/ExtensionMetadataService";
1515
import { listLocalBranches, detectDefaultTrunkBranch } from "@/node/git";
1616
import { createRuntime, IncompatibleRuntimeError } from "@/node/runtime/runtimeFactory";
17-
import { generateWorkspaceName } from "./workspaceTitleGenerator";
17+
import { generateWorkspaceName, generatePlaceholderName } from "./workspaceTitleGenerator";
1818
import { validateWorkspaceName } from "@/common/utils/validation/workspaceValidation";
1919

2020
import type {
@@ -415,23 +415,9 @@ export class WorkspaceService extends EventEmitter {
415415
| { success: false; error: string }
416416
> {
417417
try {
418-
const branchNameResult = await generateWorkspaceName(message, options.model, this.aiService);
419-
if (!branchNameResult.success) {
420-
const err = branchNameResult.error;
421-
const errorMessage =
422-
"message" in err
423-
? err.message
424-
: err.type === "api_key_not_found"
425-
? `API key not found for ${err.provider}`
426-
: err.type === "provider_not_supported"
427-
? `Provider not supported: ${err.provider}`
428-
: "raw" in err
429-
? err.raw
430-
: "Unknown error";
431-
return { success: false, error: errorMessage };
432-
}
433-
const branchName = branchNameResult.data;
434-
log.debug("Generated workspace name", { branchName });
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 });
435421

436422
const branches = await listLocalBranches(projectPath);
437423
const recommendedTrunk =
@@ -470,7 +456,7 @@ export class WorkspaceService extends EventEmitter {
470456
const initLogger = this.createInitLogger(workspaceId);
471457

472458
// Create workspace with automatic collision retry
473-
let finalBranchName = branchName;
459+
let finalBranchName = placeholderName;
474460
let createResult: { success: boolean; workspacePath?: string; error?: string };
475461

476462
for (let attempt = 0; attempt <= MAX_WORKSPACE_NAME_COLLISION_RETRIES; attempt++) {
@@ -490,7 +476,7 @@ export class WorkspaceService extends EventEmitter {
490476
attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES
491477
) {
492478
log.debug(`Workspace name collision for "${finalBranchName}", retrying with suffix`);
493-
finalBranchName = appendCollisionSuffix(branchName);
479+
finalBranchName = appendCollisionSuffix(placeholderName);
494480
continue;
495481
}
496482
break;
@@ -558,6 +544,9 @@ export class WorkspaceService extends EventEmitter {
558544

559545
void session.sendMessage(message, options);
560546

547+
// Generate AI name asynchronously and rename if successful
548+
void this.generateAndApplyAIName(workspaceId, message, finalBranchName, options.model);
549+
561550
return {
562551
success: true,
563552
workspaceId,
@@ -570,6 +559,78 @@ export class WorkspaceService extends EventEmitter {
570559
}
571560
}
572561

562+
/**
563+
* Asynchronously generates an AI workspace name and renames the workspace if successful.
564+
* This runs in the background after workspace creation to avoid blocking the UX.
565+
*/
566+
private async generateAndApplyAIName(
567+
workspaceId: string,
568+
message: string,
569+
currentName: string,
570+
model: string
571+
): Promise<void> {
572+
try {
573+
log.debug("Starting async AI name generation", { workspaceId, currentName });
574+
575+
const branchNameResult = await generateWorkspaceName(message, model, this.aiService);
576+
577+
if (!branchNameResult.success) {
578+
// AI name generation failed - keep the placeholder name
579+
const err = branchNameResult.error;
580+
const errorMessage =
581+
"message" in err
582+
? err.message
583+
: err.type === "api_key_not_found"
584+
? `API key not found for ${err.provider}`
585+
: err.type === "provider_not_supported"
586+
? `Provider not supported: ${err.provider}`
587+
: "raw" in err
588+
? err.raw
589+
: "Unknown error";
590+
log.info("AI name generation failed, keeping placeholder name", {
591+
workspaceId,
592+
currentName,
593+
error: errorMessage,
594+
});
595+
return;
596+
}
597+
598+
const aiGeneratedName = branchNameResult.data;
599+
log.debug("AI generated workspace name", { workspaceId, aiGeneratedName, currentName });
600+
601+
// Only rename if the AI name is different from current name
602+
if (aiGeneratedName === currentName) {
603+
log.debug("AI name matches placeholder, no rename needed", { workspaceId });
604+
return;
605+
}
606+
607+
// Attempt to rename the workspace
608+
const renameResult = await this.rename(workspaceId, aiGeneratedName);
609+
610+
if (!renameResult.success) {
611+
// Rename failed (e.g., collision) - keep the placeholder name
612+
log.info("Failed to rename workspace to AI-generated name", {
613+
workspaceId,
614+
aiGeneratedName,
615+
error: renameResult.error,
616+
});
617+
return;
618+
}
619+
620+
log.info("Successfully renamed workspace to AI-generated name", {
621+
workspaceId,
622+
oldName: currentName,
623+
newName: aiGeneratedName,
624+
});
625+
} catch (error) {
626+
const errorMessage = error instanceof Error ? error.message : String(error);
627+
log.error("Unexpected error in async AI name generation", {
628+
workspaceId,
629+
error: errorMessage,
630+
});
631+
}
632+
}
633+
573634
async remove(workspaceId: string, force = false): Promise<Result<void>> {
574635
// Try to remove from runtime (filesystem)
575636
try {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, it, expect } from "bun:test";
2+
import { generatePlaceholderName } from "./workspaceTitleGenerator";
3+
4+
describe("generatePlaceholderName", () => {
5+
it("should generate a git-safe name from message", () => {
6+
const result = generatePlaceholderName("Add user authentication feature");
7+
expect(result).toBe("add-user-authentication-featur");
8+
});
9+
10+
it("should handle special characters", () => {
11+
const result = generatePlaceholderName("Fix bug #123 in user/profile");
12+
expect(result).toBe("fix-bug-123-in-user-profile");
13+
});
14+
15+
it("should truncate long messages", () => {
16+
const result = generatePlaceholderName(
17+
"This is a very long message that should be truncated to fit within the maximum length"
18+
);
19+
expect(result.length).toBeLessThanOrEqual(30);
20+
expect(result).toBe("this-is-a-very-long-message-th");
21+
});
22+
23+
it("should return default name for empty/whitespace input", () => {
24+
expect(generatePlaceholderName("")).toBe("new-workspace");
25+
expect(generatePlaceholderName(" ")).toBe("new-workspace");
26+
});
27+
28+
it("should handle unicode characters", () => {
29+
const result = generatePlaceholderName("Add émojis 🚀 and accénts");
30+
expect(result).toBe("add-mojis-and-acc-nts");
31+
});
32+
33+
it("should handle only special characters", () => {
34+
const result = generatePlaceholderName("!@#$%^&*()");
35+
expect(result).toBe("new-workspace");
36+
});
37+
});

0 commit comments

Comments
 (0)