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
10 changes: 9 additions & 1 deletion src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import {
type OpenAIReasoningMode,
type ThinkingLevel,
} from "@/common/types/thinking";
import { CUSTOM_EVENTS } from "@/common/constants/events";
import { createCustomEvent, CUSTOM_EVENTS } from "@/common/constants/events";
import { isWorkspaceForkSwitchEvent } from "./utils/workspaceEvents";
import {
getAgentIdKey,
Expand Down Expand Up @@ -720,6 +720,14 @@ function AppInner() {
});
if (provider == null) {
fastModeToggleInFlightRef.current = false;
// The shortcut used to do nothing here, so it looked broken (#5693). Say why instead.
window.dispatchEvent(
createCustomEvent(CUSTOM_EVENTS.ANALYTICS_REBUILD_TOAST, {
type: "error",
title: "Fast mode",
message: `Fast mode is not available for ${model} on its current provider route, for example through a gateway or a custom base URL.`,
})
Comment thread
ThomasK33 marked this conversation as resolved.
);
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/browser/components/Tooltip/Tooltip";
import { cn } from "@/common/lib/utils";

interface SidebarCollapseButtonProps {
collapsed: boolean;
Expand Down Expand Up @@ -32,11 +33,14 @@ export const SidebarCollapseButton: React.FC<SidebarCollapseButtonProps> = ({
<button
onClick={onToggle}
aria-label={label}
className={
className={cn(
collapsed
? "text-muted hover:bg-hover hover:text-foreground flex w-full flex-1 cursor-pointer items-center justify-center bg-transparent p-0 text-xs transition-all duration-200"
: "text-muted border-dark hover:bg-hover hover:text-foreground mt-auto flex h-6 w-full cursor-pointer items-center justify-center border-t border-none bg-transparent p-0 text-xs transition-all duration-200"
}
: "text-muted border-dark hover:bg-hover hover:text-foreground mt-auto flex h-6 w-full cursor-pointer items-center justify-center border-t border-none bg-transparent p-0 text-xs transition-all duration-200",
// globals.css removes outlines, so keyboard focus needs its own ring (#5688). Inset,
// because the sidebar clips anything drawn outside the button.
"focus-visible:ring-accent focus-visible:ring-1 focus-visible:ring-inset"
)}
>
{chevron}
</button>
Expand Down
4 changes: 3 additions & 1 deletion src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -842,8 +842,10 @@ export const WorkspaceMenuBar: React.FC<WorkspaceMenuBarProps> = ({
</Button>
</PopoverTrigger>
</TooltipTrigger>
{/* Same words as the button's aria-label, so sighted and screen-reader users get one
name for this control (#5681). */}
<TooltipContent side="bottom" align="end">
More actions
Workspace actions
</TooltipContent>
</Tooltip>

Expand Down
21 changes: 21 additions & 0 deletions src/browser/features/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ import {
matchesKeybind,
formatKeybind,
KEYBINDS,
isDesktopViewportFocused,
isDialogOpen,
isEditableElement,
} from "@/browser/utils/ui/keybinds";
import {
Expand Down Expand Up @@ -1347,6 +1349,25 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
}
}, [onReady, focusMessageInput, send]);

// The creation composer shows the focus-chat hint too, but the only handler lived in the
// workspace view (useAIViewKeybinds), so the shortcut did nothing here (#5687). Same rules as
// there: capture phase, works from inputs, yields to remote desktops and open dialogs.
useEffect(() => {
if (variant !== "creation") return;
const handleFocusChat = (event: KeyboardEvent) => {
if (!matchesKeybind(event, KEYBINDS.FOCUS_CHAT) || isDesktopViewportFocused(event.target)) {
return;
}
event.preventDefault();
// isDialogOpen() misses the command palette (cmdk); focus must not move behind it.
const inCommandPalette =
event.target instanceof Element && event.target.closest("[cmdk-root]") != null;
if (!isDialogOpen() && !inCommandPalette) focusMessageInput();
};
window.addEventListener("keydown", handleFocusChat, { capture: true });
return () => window.removeEventListener("keydown", handleFocusChat, { capture: true });
}, [variant, focusMessageInput]);

useEffect(() => {
const handleGlobalKeyDown = (event: KeyboardEvent) => {
if (isEditableElement(event.target)) {
Expand Down
86 changes: 86 additions & 0 deletions tests/bugbash/repros/focusAndLabels.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// #5681, #5687, #5688, #5693: keyboard focus, labels and shortcut feedback. Each test fails on
// the old code.
import { test } from "@e2e-dev/web";
import { expect } from "e2e";
import { disableTutorials, openPlayground } from "./helpers";

test(
"Ctrl+I focuses the composer on the new-workspace screen",
{ tags: ["bugbash", "5687"] },
async ({ app, screen, browser }) => {
await browser.setViewport({ width: 1440, height: 900 });
await disableTutorials(browser);
await app.open();
const composer = screen.getByRole("textbox", "Message");
await expect(composer).toBeVisible({ timeout: 15_000 });
// The screen opens with the composer focused, so move focus away first.
await browser.evaluate(() => {
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
return null;
});
await expect(composer).not.toBeFocused();

await browser.keyboard.press("Control+i");
await expect(composer).toBeFocused();

// With the command palette open, Ctrl+I leaves focus in the palette.
await browser.keyboard.press("Control+Shift+P");
const palette = screen.getByRole("combobox", "Command palette");
await expect(palette).toBeFocused();
await browser.keyboard.press("Control+i");
await expect(palette).toBeFocused();
}
);

test(
"The right sidebar's collapse button shows a focus ring",
{ tags: ["bugbash", "5688"] },
async ({ app, screen, browser }) => {
await browser.setViewport({ width: 1440, height: 900 });
await openPlayground(app, screen, browser);
const collapse = screen
.getByRole("complementary", "Workspace insights")
.getByRole("button", "Collapse sidebar");
await expect(collapse).toBeVisible();
// A key press first, so the browser treats the next focus as keyboard focus (:focus-visible).
await browser.keyboard.press("Shift");
await collapse.focus();

await expect
.poll(() =>
browser.evaluate(() => {
const el = document.activeElement;
if (!(el instanceof HTMLElement) || !el.matches(":focus-visible"))
return "no focus-visible";
return getComputedStyle(el).boxShadow;
})
)
.not.toMatch(/^(none|no focus-visible)$/);
}
);

test(
"The workspace actions tooltip uses the button's accessible name",
{ tags: ["bugbash", "5681"] },
async ({ app, screen, browser }) => {
await browser.setViewport({ width: 1440, height: 900 });
await openPlayground(app, screen, browser);
await screen.getByRole("button", "Workspace actions").hover();

await expect(screen.getByRole("tooltip")).toHaveText("Workspace actions");
}
);

test(
"The fast mode shortcut explains why fast mode is unavailable",
{ tags: ["bugbash", "5693"] },
async ({ app, screen, browser }) => {
await browser.setViewport({ width: 1440, height: 900 });
// The seeded Anthropic provider uses a loopback base URL, so fast mode is unavailable.
await openPlayground(app, screen, browser);
await screen.getByRole("textbox", "Message").tap();

await browser.keyboard.press("Control+Shift+F");
await expect(screen.getByText(/^Fast mode is not available/)).toBeVisible();
}
);
Loading