Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,9 @@ export function branchMismatchKey(

// The mismatch banner only matters when the user is about to send: passive
// reading of an old thread carries no risk (the branch picker tint already
// covers ambient awareness). Draft content is the intent signal — composer
// focus is useless here because ChatView autofocuses the composer on every
// thread open. `wasShownForCurrentMismatch` keeps the banner mounted once
// covers ambient awareness). Draft content is the intent signal. Composer
// focus is useless here because ChatView normally autofocuses it when a thread
// opens. `wasShownForCurrentMismatch` keeps the banner mounted once
// revealed so it doesn't flicker away when the draft is cleared.
export function shouldShowBranchMismatchBanner(input: {
hasMismatch: boolean;
Expand Down
11 changes: 10 additions & 1 deletion apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ const TYPE_TO_FOCUS_INTERACTIVE_SELECTOR = [
"button",
"a[href]",
"summary",
'[role="button"]',
'[role="button"]:not([data-thread-row])',
'[role="checkbox"]',
'[role="menuitem"]',
'[role="option"]',
Expand Down Expand Up @@ -499,6 +499,7 @@ function shouldTypeToFocusComposer(event: KeyboardEvent): boolean {
if (event.key.length !== 1) return false;

if (eventPathContainsSelector(event, TYPE_TO_FOCUS_EDITABLE_SELECTOR)) return false;
if (event.key === " " && eventPathContainsSelector(event, "[data-thread-row]")) return false;
if (eventPathContainsSelector(event, TYPE_TO_FOCUS_INTERACTIVE_SELECTOR)) return false;
if (document.querySelector(TYPE_TO_FOCUS_FLOATING_LAYER_SELECTOR)) return false;

Expand Down Expand Up @@ -4103,6 +4104,14 @@ function ChatViewContent(props: ChatViewProps) {
useEffect(() => {
if (!activeThread?.id || terminalUiState.terminalOpen) return;
const frame = window.requestAnimationFrame(() => {
const activeElement = document.activeElement;
if (
activeElement instanceof HTMLElement &&
activeElement.isConnected &&
activeElement.closest("[data-thread-item]") !== null
) {
return;
}
focusComposer();
});
return () => {
Expand Down
95 changes: 77 additions & 18 deletions apps/web/src/components/LegacySidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal";
import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstraps";
import { isElectron } from "../env";
import { useOpenPrLink } from "../lib/openPullRequestLink";
import { isPreviewFocused } from "../lib/previewFocus";
import { isTerminalFocused } from "../lib/terminalFocus";
import { isMacPlatform } from "../lib/utils";
import {
Expand All @@ -98,11 +99,13 @@ import {
resolveShortcutCommand,
shortcutLabelForCommand,
shouldShowThreadJumpHintsForModifiers,
type ShortcutMatchContext,
threadJumpCommandForIndex,
threadJumpIndexFromCommand,
threadTraversalDirectionFromCommand,
} from "../keybindings";
import { isModelPickerOpen } from "../modelPickerVisibility";
import { selectActiveRightPanel, useRightPanelStore } from "../rightPanelStore";
import { useShortcutModifierState } from "../shortcutModifierState";
import { ensureLocalApi, readLocalApi } from "../localApi";
import { useComposerDraftStore } from "../composerDraftStore";
Expand Down Expand Up @@ -303,6 +306,8 @@ function buildThreadJumpLabelMap(input: {

interface SidebarThreadRowProps {
thread: SidebarThreadSummary;
keybindings: ResolvedKeybindingsConfig;
getCurrentShortcutContext: () => ShortcutMatchContext;
projectCwd: string | null;
orderedProjectThreadKeys: readonly string[];
isActive: boolean;
Expand Down Expand Up @@ -346,6 +351,7 @@ interface SidebarThreadRowProps {

export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowProps) {
const {
keybindings,
orderedProjectThreadKeys,
isActive,
openPullRequestsInRightPanel,
Expand Down Expand Up @@ -525,11 +531,29 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
);
const handleRowKeyDown = useCallback(
(event: React.KeyboardEvent) => {
const command = resolveShortcutCommand(event, keybindings, {
platform: navigator.platform,
context: props.getCurrentShortcutContext(),
});
if (command === "thread.rename") {
event.preventDefault();
event.stopPropagation();
startThreadRename(threadKey, thread.title);
return;
}
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
navigateToThread(threadRef);
},
[navigateToThread, threadRef],
[
keybindings,
navigateToThread,
props.getCurrentShortcutContext,
startThreadRename,
thread.title,
threadKey,
threadRef,
],
);
const handleRowContextMenu = useCallback(
(event: React.MouseEvent) => {
Expand Down Expand Up @@ -595,6 +619,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
},
[isActive, navigateToThread, openPrLink, openPullRequestsInRightPanel, prStatus, threadRef],
);
const rowElementRef = useRef<HTMLDivElement>(null);
const handleRenameInputRef = useCallback(
(element: HTMLInputElement | null) => {
if (element && renamingInputRef.current !== element) {
Expand All @@ -618,10 +643,12 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
event.preventDefault();
renamingCommittedRef.current = true;
void commitRename(threadRef, renamingTitle, thread.title);
window.requestAnimationFrame(() => rowElementRef.current?.focus());
} else if (event.key === "Escape") {
event.preventDefault();
renamingCommittedRef.current = true;
cancelRename();
window.requestAnimationFrame(() => rowElementRef.current?.focus());
}
},
[cancelRename, commitRename, renamingCommittedRef, renamingTitle, thread.title, threadRef],
Expand Down Expand Up @@ -681,7 +708,10 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
},
[attemptArchiveThread, threadRef],
);
const rowButtonRender = useMemo(() => <div role="button" tabIndex={0} />, []);
const rowButtonRender = useMemo(
() => <div ref={rowElementRef} role="button" tabIndex={0} data-thread-row />,
[],
);

return (
<SidebarMenuSubItem
Expand Down Expand Up @@ -907,6 +937,8 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
});

interface SidebarProjectThreadListProps {
keybindings: ResolvedKeybindingsConfig;
getCurrentShortcutContext: () => ShortcutMatchContext;
projectKey: string;
projectExpanded: boolean;
hasOverflowingThreads: boolean;
Expand Down Expand Up @@ -963,6 +995,8 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
props: SidebarProjectThreadListProps,
) {
const {
keybindings,
getCurrentShortcutContext,
projectKey,
projectExpanded,
hasOverflowingThreads,
Expand Down Expand Up @@ -1024,6 +1058,8 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
<SidebarThreadRow
key={threadKey}
thread={thread}
keybindings={keybindings}
getCurrentShortcutContext={getCurrentShortcutContext}
projectCwd={projectCwd}
orderedProjectThreadKeys={orderedProjectThreadKeys}
isActive={activeRouteThreadKey === threadKey}
Expand Down Expand Up @@ -1091,6 +1127,8 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(

interface SidebarProjectItemProps {
project: SidebarProjectSnapshot;
keybindings: ResolvedKeybindingsConfig;
getCurrentShortcutContext: () => ShortcutMatchContext;
isThreadListExpanded: boolean;
activeRouteThreadKey: string | null;
openPullRequestsInRightPanel: boolean;
Expand All @@ -1112,6 +1150,8 @@ interface SidebarProjectItemProps {
const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjectItemProps) {
const {
project,
keybindings,
getCurrentShortcutContext,
isThreadListExpanded,
activeRouteThreadKey,
openPullRequestsInRightPanel,
Expand Down Expand Up @@ -2022,25 +2062,21 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
const commitRename = useCallback(
async (threadRef: ScopedThreadRef, newTitle: string, originalTitle: string) => {
const threadKey = scopedThreadKey(threadRef);
const finishRename = () => {
setRenamingThreadKey((current) => {
if (current !== threadKey) return current;
renamingInputRef.current = null;
return null;
});
};
setRenamingThreadKey((current) => {
if (current !== threadKey) return current;
renamingInputRef.current = null;
return null;
});

const trimmed = newTitle.trim();
if (trimmed.length === 0) {
toastManager.add({
type: "warning",
title: "Thread title cannot be empty",
});
finishRename();
return;
}
if (trimmed === originalTitle) {
finishRename();
return;
}
const result = await updateThreadMetadata({
Expand All @@ -2060,7 +2096,6 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
}),
);
}
finishRename();
},
[updateThreadMetadata],
);
Expand Down Expand Up @@ -2368,6 +2403,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
</div>

<SidebarProjectThreadList
keybindings={keybindings}
getCurrentShortcutContext={getCurrentShortcutContext}
projectKey={project.projectKey}
projectExpanded={projectExpanded}
hasOverflowingThreads={hasOverflowingThreads}
Expand Down Expand Up @@ -2773,6 +2810,8 @@ function SortableProjectItem({
}

interface SidebarProjectsContentProps {
keybindings: ResolvedKeybindingsConfig;
getCurrentShortcutContext: () => ShortcutMatchContext;
showArm64IntelBuildWarning: boolean;
arm64IntelBuildWarningDescription: string | null;
desktopUpdateButtonAction: "download" | "install" | "none";
Expand Down Expand Up @@ -2815,6 +2854,8 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
props: SidebarProjectsContentProps,
) {
const {
keybindings,
getCurrentShortcutContext,
showArm64IntelBuildWarning,
arm64IntelBuildWarningDescription,
desktopUpdateButtonAction,
Expand Down Expand Up @@ -2976,6 +3017,8 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
{(dragHandleProps) => (
<SidebarProjectItem
project={project}
keybindings={keybindings}
getCurrentShortcutContext={getCurrentShortcutContext}
isThreadListExpanded={expandedThreadListsByProject.has(project.projectKey)}
activeRouteThreadKey={
activeRouteProjectKey === project.projectKey ? routeThreadKey : null
Expand Down Expand Up @@ -3009,6 +3052,8 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
<SidebarProjectListRow
key={project.projectKey}
project={project}
keybindings={keybindings}
getCurrentShortcutContext={getCurrentShortcutContext}
isThreadListExpanded={expandedThreadListsByProject.has(project.projectKey)}
activeRouteThreadKey={
activeRouteProjectKey === project.projectKey ? routeThreadKey : null
Expand Down Expand Up @@ -3067,6 +3112,8 @@ export default function LegacySidebar() {
[routeDraftThread, routeTarget],
);
const routeThreadKey = routeThreadRef ? scopedThreadKey(routeThreadRef) : null;
const routeThreadRefForShortcuts = useRef(routeThreadRef);
routeThreadRefForShortcuts.current = routeThreadRef;
const routeTerminalOpen = useTerminalUiStateStore((state) =>
routeThreadRef
? selectThreadTerminalUiState(state.terminalUiStateByThreadKey, routeThreadRef).terminalOpen
Expand Down Expand Up @@ -3205,14 +3252,24 @@ export default function LegacySidebar() {
}
return next;
}, [sidebarThreads, physicalToLogicalKey, projectPhysicalKeyByScopedRef]);
const getCurrentSidebarShortcutContext = useCallback(
() => ({
const getCurrentSidebarShortcutContext = useCallback((): ShortcutMatchContext => {
const activeThreadRef = routeThreadRefForShortcuts.current;
return {
terminalFocus: isTerminalFocused(),
terminalOpen: routeTerminalOpen,
terminalOpen: activeThreadRef
? selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
activeThreadRef,
).terminalOpen
: false,
previewFocus: isPreviewFocused(),
previewOpen: activeThreadRef
? selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, activeThreadRef) ===
"preview"
: false,
modelPickerOpen: isModelPickerOpen(),
}),
[routeTerminalOpen],
);
};
}, []);
const newThreadShortcutLabelOptions = useMemo(
() => ({
platform,
Expand Down Expand Up @@ -3675,6 +3732,8 @@ export default function LegacySidebar() {
<SidebarChromeHeader isElectron={isElectron} />

<SidebarProjectsContent
keybindings={keybindings}
getCurrentShortcutContext={getCurrentSidebarShortcutContext}
showArm64IntelBuildWarning={showArm64IntelBuildWarning}
arm64IntelBuildWarningDescription={arm64IntelBuildWarningDescription}
desktopUpdateButtonAction={desktopUpdateButtonAction}
Expand Down
Loading
Loading