diff --git a/src/browser/App.tsx b/src/browser/App.tsx
index 3f7a001158..9f9a7ba366 100644
--- a/src/browser/App.tsx
+++ b/src/browser/App.tsx
@@ -90,7 +90,7 @@ function AppInner() {
loading,
setWorkspaceMetadata,
removeWorkspace,
- renameWorkspace,
+ updateWorkspaceTitle,
refreshWorkspaceMetadata,
selectedWorkspace,
setSelectedWorkspace,
@@ -555,9 +555,9 @@ function AppInner() {
[removeWorkspace]
);
- const renameWorkspaceFromPalette = useCallback(
- async (workspaceId: string, newName: string) => renameWorkspace(workspaceId, newName),
- [renameWorkspace]
+ const updateTitleFromPalette = useCallback(
+ async (workspaceId: string, newTitle: string) => updateWorkspaceTitle(workspaceId, newTitle),
+ [updateWorkspaceTitle]
);
const addProjectFromPalette = useCallback(() => {
@@ -594,7 +594,7 @@ function AppInner() {
getBranchesForProject,
onSelectWorkspace: selectWorkspaceFromPalette,
onRemoveWorkspace: removeWorkspaceFromPalette,
- onRenameWorkspace: renameWorkspaceFromPalette,
+ onUpdateTitle: updateTitleFromPalette,
onAddProject: addProjectFromPalette,
onRemoveProject: removeProjectFromPalette,
onToggleSidebar: toggleSidebarFromPalette,
diff --git a/src/browser/components/ChatInputToasts.tsx b/src/browser/components/ChatInputToasts.tsx
index 22f2cc2728..b7f1ef48d8 100644
--- a/src/browser/components/ChatInputToasts.tsx
+++ b/src/browser/components/ChatInputToasts.tsx
@@ -63,26 +63,6 @@ export const createCommandToast = (parsed: ParsedCommand): Toast | null => {
),
};
- case "fork-help":
- return {
- id: Date.now().toString(),
- type: "error",
- title: "Fork Command",
- message: "Fork current workspace with a new name",
- solution: (
- <>
- Usage:
- /fork <new-name> [optional start message]
-
-
- Examples:
- /fork experiment-branch
-
- /fork refactor Continue with refactoring approach
- >
- ),
- };
-
case "command-missing-args":
return {
id: Date.now().toString(),
diff --git a/src/browser/components/ProjectSidebar.tsx b/src/browser/components/ProjectSidebar.tsx
index e920c8ad5d..d9a632bcf4 100644
--- a/src/browser/components/ProjectSidebar.tsx
+++ b/src/browser/components/ProjectSidebar.tsx
@@ -25,7 +25,13 @@ import {
reorderProjects,
normalizeOrder,
} from "@/common/utils/projectOrdering";
-import { matchesKeybind, formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds";
+import {
+ matchesKeybind,
+ formatKeybind,
+ isEditableElement,
+ KEYBINDS,
+} from "@/browser/utils/ui/keybinds";
+import { useAPI } from "@/browser/contexts/API";
import { PlatformPaths } from "@/common/utils/paths";
import {
partitionWorkspacesByAge,
@@ -47,13 +53,14 @@ import type { Secret } from "@/common/types/secrets";
import { WorkspaceListItem, type WorkspaceSelection } from "./WorkspaceListItem";
import { WorkspaceStatusIndicator } from "./WorkspaceStatusIndicator";
-import { RenameProvider } from "@/browser/contexts/WorkspaceRenameContext";
+import { TitleEditProvider, useTitleEdit } from "@/browser/contexts/WorkspaceTitleEditContext";
import { useProjectContext } from "@/browser/contexts/ProjectContext";
import { ChevronRight, CircleHelp, KeyRound } from "lucide-react";
import { MUX_HELP_CHAT_WORKSPACE_ID } from "@/common/constants/muxChat";
import { useWorkspaceActions } from "@/browser/contexts/WorkspaceContext";
import { useRouter } from "@/browser/contexts/RouterContext";
import { usePopoverError } from "@/browser/hooks/usePopoverError";
+import { forkWorkspace } from "@/browser/utils/chatCommands";
import { PopoverError } from "./PopoverError";
import { SectionHeader } from "./SectionHeader";
import { AddSectionButton } from "./AddSectionButton";
@@ -319,6 +326,52 @@ function MuxChatStatusIndicator() {
);
}
+/**
+ * Handles F2 (edit title) and Shift+F2 (generate new title) keybinds.
+ * Rendered inside TitleEditProvider so it can access useTitleEdit().
+ */
+function SidebarTitleEditKeybinds(props: {
+ selectedWorkspace: WorkspaceSelection | undefined;
+ sortedWorkspacesByProject: Map;
+}) {
+ const { requestEdit, wrapGenerateTitle } = useTitleEdit();
+ const { api } = useAPI();
+
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (!props.selectedWorkspace) return;
+ if (isEditableElement(e.target)) return;
+ const wsId = props.selectedWorkspace.workspaceId;
+ if (wsId === MUX_HELP_CHAT_WORKSPACE_ID) return;
+
+ if (matchesKeybind(e, KEYBINDS.EDIT_WORKSPACE_TITLE)) {
+ e.preventDefault();
+ const meta = props.sortedWorkspacesByProject
+ .get(props.selectedWorkspace.projectPath)
+ ?.find((m) => m.id === wsId);
+ const displayTitle = meta?.title ?? meta?.name ?? "";
+ requestEdit(wsId, displayTitle);
+ } else if (matchesKeybind(e, KEYBINDS.GENERATE_WORKSPACE_TITLE)) {
+ e.preventDefault();
+ wrapGenerateTitle(
+ wsId,
+ () => api?.workspace.regenerateTitle({ workspaceId: wsId }) ?? Promise.resolve()
+ );
+ }
+ };
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [
+ props.selectedWorkspace,
+ props.sortedWorkspacesByProject,
+ requestEdit,
+ wrapGenerateTitle,
+ api,
+ ]);
+
+ return null;
+}
+
interface ProjectSidebarProps {
collapsed: boolean;
onToggleCollapsed: () => void;
@@ -344,7 +397,7 @@ const ProjectSidebarInner: React.FC = ({
setSelectedWorkspace: onSelectWorkspace,
archiveWorkspace: onArchiveWorkspace,
removeWorkspace,
- renameWorkspace: onRenameWorkspace,
+ updateWorkspaceTitle: onUpdateTitle,
refreshWorkspaceMetadata,
pendingNewWorkspaceProject,
pendingNewWorkspaceDraftId,
@@ -356,6 +409,7 @@ const ProjectSidebarInner: React.FC = ({
} = useWorkspaceActions();
const workspaceStore = useWorkspaceStoreRaw();
const { navigateToProject } = useRouter();
+ const { api } = useAPI();
// Get project state and operations from context
const {
@@ -467,6 +521,7 @@ const ProjectSidebarInner: React.FC = ({
const [archivingWorkspaceIds, setArchivingWorkspaceIds] = useState>(new Set());
const [removingWorkspaceIds, setRemovingWorkspaceIds] = useState>(new Set());
const workspaceArchiveError = usePopoverError();
+ const workspaceForkError = usePopoverError();
const workspaceRemoveError = usePopoverError();
const [archiveConfirmation, setArchiveConfirmation] = useState<{
workspaceId: string;
@@ -522,6 +577,35 @@ const ProjectSidebarInner: React.FC = ({
}
};
+ const handleForkWorkspace = useCallback(
+ async (workspaceId: string, buttonElement?: HTMLElement) => {
+ if (!api) {
+ workspaceForkError.showError(workspaceId, "Not connected to server");
+ return;
+ }
+
+ const result = await forkWorkspace({
+ client: api,
+ sourceWorkspaceId: workspaceId,
+ });
+ if (result.success) {
+ return;
+ }
+
+ let anchor: { top: number; left: number } | undefined;
+ if (buttonElement) {
+ const rect = buttonElement.getBoundingClientRect();
+ anchor = {
+ top: rect.top + window.scrollY,
+ left: rect.right + 10,
+ };
+ }
+
+ workspaceForkError.showError(workspaceId, result.error ?? "Failed to fork chat", anchor);
+ },
+ [api, workspaceForkError]
+ );
+
const performArchiveWorkspace = useCallback(
async (workspaceId: string, buttonElement?: HTMLElement) => {
// Mark workspace as being archived for UI feedback
@@ -740,7 +824,11 @@ const ProjectSidebarInner: React.FC = ({
}, [selectedWorkspace, handleAddWorkspace, handleArchiveWorkspace]);
return (
-
+
+
@@ -1011,6 +1099,7 @@ const ProjectSidebarInner: React.FC = ({
metadata.isRemoving === true
}
onSelectWorkspace={handleSelectWorkspace}
+ onForkWorkspace={handleForkWorkspace}
onArchiveWorkspace={handleArchiveWorkspace}
onCancelCreation={handleCancelWorkspaceCreation}
depth={depthByWorkspaceId[metadata.id] ?? 0}
@@ -1377,6 +1466,11 @@ const ProjectSidebarInner: React.FC = ({
prefix="Failed to archive chat"
onDismiss={workspaceArchiveError.clearError}
/>
+
= ({
/>
-
+
);
};
diff --git a/src/browser/components/Settings/sections/KeybindsSection.tsx b/src/browser/components/Settings/sections/KeybindsSection.tsx
index 8946c20e91..ea587d222e 100644
--- a/src/browser/components/Settings/sections/KeybindsSection.tsx
+++ b/src/browser/components/Settings/sections/KeybindsSection.tsx
@@ -17,6 +17,8 @@ const KEYBIND_LABELS: Record = {
FOCUS_INPUT_I: "Focus input (i)",
FOCUS_INPUT_A: "Focus input (a)",
NEW_WORKSPACE: "New workspace",
+ EDIT_WORKSPACE_TITLE: "Edit workspace title",
+ GENERATE_WORKSPACE_TITLE: "Generate new title",
ARCHIVE_WORKSPACE: "Archive workspace",
JUMP_TO_BOTTOM: "Jump to bottom",
NEXT_WORKSPACE: "Next workspace",
@@ -98,6 +100,8 @@ const KEYBIND_GROUPS: Array<{ label: string; keys: Array
label: "Navigation",
keys: [
"NEW_WORKSPACE",
+ "EDIT_WORKSPACE_TITLE",
+ "GENERATE_WORKSPACE_TITLE",
"ARCHIVE_WORKSPACE",
"NEXT_WORKSPACE",
"PREV_WORKSPACE",
diff --git a/src/browser/components/WorkspaceListItem.tsx b/src/browser/components/WorkspaceListItem.tsx
index 9eea57ddd6..4d25ba76b6 100644
--- a/src/browser/components/WorkspaceListItem.tsx
+++ b/src/browser/components/WorkspaceListItem.tsx
@@ -1,4 +1,4 @@
-import { useRename } from "@/browser/contexts/WorkspaceRenameContext";
+import { useTitleEdit } from "@/browser/contexts/WorkspaceTitleEditContext";
import { stopKeyboardPropagation } from "@/browser/utils/events";
import { cn } from "@/common/lib/utils";
import { useGitStatus } from "@/browser/stores/GitStatusStore";
@@ -16,7 +16,7 @@ import { WorkspaceHoverPreview } from "./WorkspaceHoverPreview";
import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip";
import { HoverCard, HoverCardTrigger, HoverCardContent } from "./ui/hover-card";
import { Popover, PopoverContent, PopoverTrigger, PopoverAnchor } from "./ui/popover";
-import { Pencil, Trash2, Ellipsis, Loader2, Link2 } from "lucide-react";
+import { Pencil, Trash2, Ellipsis, Loader2, Link2, Sparkles, GitBranch } from "lucide-react";
import { WorkspaceStatusIndicator } from "./WorkspaceStatusIndicator";
import { Shimmer } from "./ai-elements/shimmer";
import { ArchiveIcon } from "./icons/ArchiveIcon";
@@ -24,6 +24,7 @@ import { WORKSPACE_DRAG_TYPE, type WorkspaceDragItem } from "./WorkspaceSectionD
import { useLinkSharingEnabled } from "@/browser/contexts/TelemetryEnabledContext";
import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds";
import { ShareTranscriptDialog } from "./ShareTranscriptDialog";
+import { useAPI } from "@/browser/contexts/API";
const RADIX_PORTAL_WRAPPER_SELECTOR = "[data-radix-popper-content-wrapper]" as const;
@@ -75,6 +76,7 @@ export interface WorkspaceListItemProps extends WorkspaceListItemBaseProps {
/** Section ID this workspace belongs to (for drag-drop targeting) */
sectionId?: string;
onSelectWorkspace: (selection: WorkspaceSelection) => void;
+ onForkWorkspace: (workspaceId: string, button: HTMLElement) => Promise;
onArchiveWorkspace: (workspaceId: string, button: HTMLElement) => Promise;
onCancelCreation: (workspaceId: string) => Promise;
}
@@ -348,6 +350,7 @@ function RegularWorkspaceListItemInner(props: WorkspaceListItemProps) {
depth,
sectionId,
onSelectWorkspace,
+ onForkWorkspace,
onArchiveWorkspace,
onCancelCreation,
} = props;
@@ -362,8 +365,17 @@ function RegularWorkspaceListItemInner(props: WorkspaceListItemProps) {
const { isUnread } = useWorkspaceUnread(workspaceId);
const gitStatus = useGitStatus(workspaceId);
- // Get title edit context (renamed from rename context since we now edit titles, not names)
- const { editingWorkspaceId, requestRename, confirmRename, cancelRename } = useRename();
+ // Get title edit context — manages inline title editing state across the sidebar
+ const {
+ editingWorkspaceId,
+ requestEdit,
+ confirmEdit,
+ cancelEdit,
+ generatingTitleWorkspaceId,
+ wrapGenerateTitle,
+ } = useTitleEdit();
+ const isGeneratingTitle = generatingTitleWorkspaceId === workspaceId;
+ const { api } = useAPI();
// Local state for title editing
const [editingTitle, setEditingTitle] = useState("");
@@ -407,7 +419,7 @@ function RegularWorkspaceListItemInner(props: WorkspaceListItemProps) {
// so it works even when the sidebar is collapsed and list items are unmounted.
const startEditing = () => {
- if (requestRename(workspaceId, displayTitle)) {
+ if (requestEdit(workspaceId, displayTitle)) {
setEditingTitle(displayTitle);
setTitleError(null);
}
@@ -419,7 +431,7 @@ function RegularWorkspaceListItemInner(props: WorkspaceListItemProps) {
return;
}
- const result = await confirmRename(workspaceId, editingTitle);
+ const result = await confirmEdit(workspaceId, editingTitle);
if (!result.success) {
setTitleError(result.error ?? "Failed to update title");
} else {
@@ -428,7 +440,7 @@ function RegularWorkspaceListItemInner(props: WorkspaceListItemProps) {
};
const handleCancelEdit = () => {
- cancelRename();
+ cancelEdit();
setEditingTitle("");
setTitleError(null);
};
@@ -695,9 +707,48 @@ function RegularWorkspaceListItemInner(props: WorkspaceListItemProps) {
>
- Edit chat title
+ Edit chat title{" "}
+
+ ({formatKeybind(KEYBINDS.EDIT_WORKSPACE_TITLE)})
+
+
+
+
+ {!isMuxHelpChat && (
+ // Expose fork in the row menu so users can quickly branch off a chat
+ // without needing to remember the /fork slash command.
+
+ )}
{/* Share transcript link (gated on telemetry/link-sharing being enabled). */}
{linkSharingEnabled === true && !isMuxHelpChat && (