Skip to content
10 changes: 5 additions & 5 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function AppInner() {
loading,
setWorkspaceMetadata,
removeWorkspace,
renameWorkspace,
updateWorkspaceTitle,
refreshWorkspaceMetadata,
selectedWorkspace,
setSelectedWorkspace,
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -594,7 +594,7 @@ function AppInner() {
getBranchesForProject,
onSelectWorkspace: selectWorkspaceFromPalette,
onRemoveWorkspace: removeWorkspaceFromPalette,
onRenameWorkspace: renameWorkspaceFromPalette,
onUpdateTitle: updateTitleFromPalette,
onAddProject: addProjectFromPalette,
onRemoveProject: removeProjectFromPalette,
onToggleSidebar: toggleSidebarFromPalette,
Expand Down
20 changes: 0 additions & 20 deletions src/browser/components/ChatInputToasts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
<>
<SolutionLabel>Usage:</SolutionLabel>
/fork &lt;new-name&gt; [optional start message]
<br />
<br />
<SolutionLabel>Examples:</SolutionLabel>
/fork experiment-branch
<br />
/fork refactor Continue with refactoring approach
</>
),
};

case "command-missing-args":
return {
id: Date.now().toString(),
Expand Down
104 changes: 99 additions & 5 deletions src/browser/components/ProjectSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";
Expand Down Expand Up @@ -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<string, FrontendWorkspaceMetadata[]>;
}) {
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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prefill title when entering edit mode from keyboard shortcut

The F2 handler only calls requestEdit, but the inline input value is initialized in WorkspaceListItem.startEditing() and that path is bypassed here. As a result, pressing F2 opens edit mode with an empty input instead of the current title, and the subsequent blur/Enter path immediately trips the "Title cannot be empty" validation unless the user retypes the whole title manually. This breaks the advertised keyboard edit flow for selected workspaces.

Useful? React with πŸ‘Β / πŸ‘Ž.

} 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;
Expand All @@ -344,7 +397,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
setSelectedWorkspace: onSelectWorkspace,
archiveWorkspace: onArchiveWorkspace,
removeWorkspace,
renameWorkspace: onRenameWorkspace,
updateWorkspaceTitle: onUpdateTitle,
refreshWorkspaceMetadata,
pendingNewWorkspaceProject,
pendingNewWorkspaceDraftId,
Expand All @@ -356,6 +409,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
} = useWorkspaceActions();
const workspaceStore = useWorkspaceStoreRaw();
const { navigateToProject } = useRouter();
const { api } = useAPI();

// Get project state and operations from context
const {
Expand Down Expand Up @@ -467,6 +521,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
const [archivingWorkspaceIds, setArchivingWorkspaceIds] = useState<Set<string>>(new Set());
const [removingWorkspaceIds, setRemovingWorkspaceIds] = useState<Set<string>>(new Set());
const workspaceArchiveError = usePopoverError();
const workspaceForkError = usePopoverError();
const workspaceRemoveError = usePopoverError();
const [archiveConfirmation, setArchiveConfirmation] = useState<{
workspaceId: string;
Expand Down Expand Up @@ -522,6 +577,35 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
}
};

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
Expand Down Expand Up @@ -740,7 +824,11 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
}, [selectedWorkspace, handleAddWorkspace, handleArchiveWorkspace]);

return (
<RenameProvider onRenameWorkspace={onRenameWorkspace}>
<TitleEditProvider onUpdateTitle={onUpdateTitle}>
<SidebarTitleEditKeybinds
selectedWorkspace={selectedWorkspace ?? undefined}
sortedWorkspacesByProject={sortedWorkspacesByProject}
/>
<DndProvider backend={HTML5Backend}>
<ProjectDragLayer />
<WorkspaceDragLayer />
Expand Down Expand Up @@ -1011,6 +1099,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
metadata.isRemoving === true
}
onSelectWorkspace={handleSelectWorkspace}
onForkWorkspace={handleForkWorkspace}
onArchiveWorkspace={handleArchiveWorkspace}
onCancelCreation={handleCancelWorkspaceCreation}
depth={depthByWorkspaceId[metadata.id] ?? 0}
Expand Down Expand Up @@ -1377,6 +1466,11 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
prefix="Failed to archive chat"
onDismiss={workspaceArchiveError.clearError}
/>
<PopoverError
error={workspaceForkError.error}
prefix="Failed to fork chat"
onDismiss={workspaceForkError.clearError}
/>
<PopoverError
error={workspaceRemoveError.error}
prefix="Failed to cancel workspace creation"
Expand All @@ -1394,7 +1488,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
/>
</div>
</DndProvider>
</RenameProvider>
</TitleEditProvider>
);
};

Expand Down
4 changes: 4 additions & 0 deletions src/browser/components/Settings/sections/KeybindsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const KEYBIND_LABELS: Record<keyof typeof KEYBINDS, string> = {
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",
Expand Down Expand Up @@ -98,6 +100,8 @@ const KEYBIND_GROUPS: Array<{ label: string; keys: Array<keyof typeof KEYBINDS>
label: "Navigation",
keys: [
"NEW_WORKSPACE",
"EDIT_WORKSPACE_TITLE",
"GENERATE_WORKSPACE_TITLE",
"ARCHIVE_WORKSPACE",
"NEXT_WORKSPACE",
"PREV_WORKSPACE",
Expand Down
Loading
Loading