Skip to content

Commit f085455

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 e729c9a commit f085455

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

437423
const branches = await listLocalBranches(projectPath);
438424
const recommendedTrunk =
@@ -471,7 +457,7 @@ export class WorkspaceService extends EventEmitter {
471457
const initLogger = this.createInitLogger(workspaceId);
472458

473459
// Create workspace with automatic collision retry
474-
let finalBranchName = branchName;
460+
let finalBranchName = placeholderName;
475461
let createResult: { success: boolean; workspacePath?: string; error?: string };
476462

477463
for (let attempt = 0; attempt <= MAX_WORKSPACE_NAME_COLLISION_RETRIES; attempt++) {
@@ -491,7 +477,7 @@ export class WorkspaceService extends EventEmitter {
491477
attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES
492478
) {
493479
log.debug(`Workspace name collision for "${finalBranchName}", retrying with suffix`);
494-
finalBranchName = appendCollisionSuffix(branchName);
480+
finalBranchName = appendCollisionSuffix(placeholderName);
495481
continue;
496482
}
497483
break;
@@ -559,6 +545,9 @@ export class WorkspaceService extends EventEmitter {
559545

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

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

563+
/**
564+
* Asynchronously generates an AI workspace name and renames the workspace if successful.
565+
* This runs in the background after workspace creation to avoid blocking the UX.
566+
*/
567+
private async generateAndApplyAIName(
568+
workspaceId: string,
569+
message: string,
570+
currentName: string,
571+
model: string
572+
): Promise<void> {
573+
try {
574+
log.debug("Starting async AI name generation", { workspaceId, currentName });
575+
576+
const branchNameResult = await generateWorkspaceName(message, model, this.aiService);
577+
578+
if (!branchNameResult.success) {
579+
// AI name generation failed - keep the placeholder name
580+
const err = branchNameResult.error;
581+
const errorMessage =
582+
"message" in err
583+
? err.message
584+
: err.type === "api_key_not_found"
585+
? `API key not found for ${err.provider}`
586+
: err.type === "provider_not_supported"
587+
? `Provider not supported: ${err.provider}`
588+
: "raw" in err
589+
? err.raw
590+
: "Unknown error";
591+
log.info("AI name generation failed, keeping placeholder name", {
592+
workspaceId,
593+
currentName,
594+
error: errorMessage,
595+
});
596+
return;
597+
}
598+
599+
const aiGeneratedName = branchNameResult.data;
600+
log.debug("AI generated workspace name", { workspaceId, aiGeneratedName, currentName });
601+
602+
// Only rename if the AI name is different from current name
603+
if (aiGeneratedName === currentName) {
604+
log.debug("AI name matches placeholder, no rename needed", { workspaceId });
605+
return;
606+
}
607+
608+
// Attempt to rename the workspace
609+
const renameResult = await this.rename(workspaceId, aiGeneratedName);
610+
611+
if (!renameResult.success) {
612+
// Rename failed (e.g., collision) - keep the placeholder name
613+
log.info("Failed to rename workspace to AI-generated name", {
614+
workspaceId,
615+
aiGeneratedName,
616+
error: renameResult.error,
617+
});
618+
return;
619+
}
620+
621+
log.info("Successfully renamed workspace to AI-generated name", {
622+
workspaceId,
623+
oldName: currentName,
624+
newName: aiGeneratedName,
625+
});
626+
} catch (error) {
627+
const errorMessage = error instanceof Error ? error.message : String(error);
628+
log.error("Unexpected error in async AI name generation", {
629+
workspaceId,
630+
error: errorMessage,
631+
});
632+
}
633+
}
634+
574635
async remove(workspaceId: string, force = false): Promise<Result<void>> {
575636
// Try to remove from runtime (filesystem)
576637
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)