Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 14 additions & 16 deletions e2e-tests/app_search.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ test("app search - basic search dialog functionality", async ({ po }) => {
const dialog = po.page.getByTestId("app-search-dialog");
await dialog.waitFor({ state: "visible", timeout: 10000 });

// Test 2: Close dialog with Ctrl+K (shortcut toggles)
await po.page.keyboard.press("Control+k");
// Test 2: Close the dedicated dialog with Escape
await po.page.keyboard.press("Escape");
await dialog.waitFor({ state: "hidden", timeout: 5000 });

// Test 3: Open dialog again with Ctrl+K (shortcut toggles)
await po.page.keyboard.press("Control+k");
// Test 3: Open the dedicated dialog again from its button
await searchButton.click();
await dialog.waitFor({ state: "visible", timeout: 10000 });

// Test 4: Search for specific term
Expand Down Expand Up @@ -103,28 +103,26 @@ test("app search - search functionality with different terms", async ({
await po.page.keyboard.press("Escape");
});

test("app search - keyboard shortcut functionality", async ({ po }) => {
test("app search - command shortcut is owned by the global palette", async ({
po,
}) => {
await po.setUp({ autoApprove: true });

// Create an app first
await po.sendPrompt("create sample application", { timeout: Timeout.LONG });
await po.navigation.goToAppsTab();

// Test keyboard shortcut (Ctrl+K) to open dialog
// Ctrl+K opens the chat-scoped global palette, not the app search dialog.
await po.page.keyboard.press("Control+k");
await po.page.getByTestId("app-search-dialog").waitFor();
await po.page.getByTestId("command-palette").waitFor();
await po.page
.getByTestId("command-palette-input")
.waitFor({ state: "visible" });
await po.page.getByTestId("app-search-dialog").waitFor({ state: "hidden" });

// Close with escape
await po.page.keyboard.press("Escape");
await po.page.getByTestId("app-search-dialog").waitFor({ state: "hidden" });

// Test keyboard shortcut again
await po.page.keyboard.press("Control+k");
await po.page.getByTestId("app-search-dialog").waitFor();

// Close with Ctrl+K (toggle)
await po.page.keyboard.press("Control+k");
await po.page.getByTestId("app-search-dialog").waitFor({ state: "hidden" });
await po.page.getByTestId("command-palette").waitFor({ state: "hidden" });
});

test("app search - navigation and selection", async ({ po }) => {
Expand Down
46 changes: 46 additions & 0 deletions e2e-tests/command_palette.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { expect } from "@playwright/test";
import { test } from "./helpers/test_helper";

test("command palette supports scoped chat and unfiltered configuration search", async ({
po,
}) => {
await po.setUp();
await po.sendPrompt("tc=1");

await po.page.keyboard.press("Control+k");
const palette = po.page.getByTestId("command-palette");
const input = po.page.getByTestId("command-palette-input");
await expect(palette).toBeVisible();
await expect(input).toHaveValue("chat: ");

await input.fill("chat: tc=1");
const chatResult = po.page.getByTestId(/^command-palette-chat-/).first();
await expect(chatResult).toBeVisible();
await chatResult.click();
await expect(palette).not.toBeVisible();

await po.page.keyboard.press("Control+p");
await expect(input).toHaveValue("");
await input.fill("Theme");
await po.page.getByTestId("command-palette-setting-setting-theme").click();
await expect(po.page).toHaveURL(/\/settings/);
await expect(po.page.locator("#setting-theme")).toHaveClass(
Comment thread
keppo-bot[bot] marked this conversation as resolved.
Outdated
/settings-highlight/,
);

await po.page.keyboard.press("Control+p");
await input.fill("environment variables");
await po.page
.getByTestId("command-palette-app-setting-environment-variables")
.click();
await expect(po.page).toHaveURL(/\/chat/);
await expect(
po.page.locator("#app-config-environment-variables"),
).toBeVisible();
await expect(
po.page.locator("#app-config-environment-variables"),
).toHaveClass(/settings-highlight/);

await po.page.getByTestId("command-palette-trigger").click();
await expect(input).toHaveValue("");
});
22 changes: 21 additions & 1 deletion src/app/TitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,13 @@ import {
useFirstPromptSaga,
} from "@/first_prompt/FirstPromptProvider";
import type { UserSettings } from "@/lib/schemas";
import { Search } from "lucide-react";

export const TitleBar = () => {
export const TitleBar = ({
onOpenCommandPalette,
}: {
onOpenCommandPalette?: () => void;
}) => {
const [selectedAppId] = useAtom(selectedAppIdAtom);
const selectedChatId = useAtomValue(selectedChatIdAtom);
const { hasArmedPayload } = useFirstPromptSaga();
Expand Down Expand Up @@ -129,6 +134,21 @@ export const TitleBar = () => {
<TooltipContent>{displayText}</TooltipContent>
</Tooltip>
{isDyadPro && <DyadProButton isDyadProEnabled={isDyadProEnabled} />}
<Button
type="button"
variant="outline"
size="sm"
className="no-app-region-drag ml-2 flex h-7 gap-1.5 px-2 text-xs"
onClick={onOpenCommandPalette}
aria-label="Open command palette"
data-testid="command-palette-trigger"
>
<Search className="size-3.5" />
<span className="hidden @3xl:inline">Commands</span>
<kbd className="hidden text-[10px] text-muted-foreground @xl:inline">
{platform === "darwin" ? "⌘P" : "Ctrl P"}
</kbd>
</Button>
</div>

<div className="flex-1 min-w-0 overflow-hidden self-end">
Expand Down
53 changes: 51 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { ThemeProvider } from "../contexts/ThemeContext";
import { DeepLinkProvider } from "../contexts/DeepLinkContext";
import { Toaster } from "sonner";
import { TitleBar } from "./TitleBar";
import { useEffect, useMemo, type ReactNode } from "react";
import {
useCallback,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
import { useAppOutputSubscription } from "@/hooks/useRunApp";
import { useAtomValue, useSetAtom } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
Expand Down Expand Up @@ -49,6 +55,8 @@ import { useSyncDefaultChatMode } from "@/hooks/useSyncDefaultChatMode";
import { PreviewErrorFacadeProvider } from "@/app_wiring/preview_error_facade";
import { usePreviewErrorFacade } from "@/app_wiring/preview_error_facade";
import { PackageManagerWarningProvider } from "@/package_manager_warnings/PackageManagerWarningProvider";
import { CommandPalette } from "@/components/CommandPalette";
import { CHAT_SCOPE_PREFIX } from "@/lib/commandPalette";

export default function RootLayout({ children }: { children: ReactNode }) {
const { streamMessage } = useStreamChat({ hasChatId: false });
Expand Down Expand Up @@ -111,8 +119,43 @@ function RootLayoutContent({ children }: { children: ReactNode }) {
selectedComponentsPreviewAtom,
);
const selectedAppId = useAtomValue(selectedAppIdAtom);
const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
const [commandPaletteQuery, setCommandPaletteQuery] = useState("");
useSyncDefaultChatMode();

const openCommandPalette = useCallback((query: string = "") => {
setCommandPaletteQuery(query);
setIsCommandPaletteOpen(true);
}, []);

const handleCommandPaletteOpenChange = useCallback((open: boolean) => {
setIsCommandPaletteOpen(open);
if (!open) setCommandPaletteQuery("");
}, []);

useEffect(() => {
const handleCommandPaletteShortcut = (event: KeyboardEvent) => {
if (
!(event.metaKey || event.ctrlKey) ||
event.altKey ||
event.shiftKey ||
(event.key.toLowerCase() !== "k" && event.key.toLowerCase() !== "p")
) {
return;
}

event.preventDefault();
Comment thread
keppo-bot[bot] marked this conversation as resolved.
if (event.repeat) return;
openCommandPalette(
event.key.toLowerCase() === "k" ? CHAT_SCOPE_PREFIX : "",
);
};

window.addEventListener("keydown", handleCommandPaletteShortcut);
return () =>
window.removeEventListener("keydown", handleCommandPaletteShortcut);
}, [openCommandPalette]);

// Initialize plan events listener
usePlanEvents();
useIntegrationEvents();
Expand Down Expand Up @@ -194,7 +237,7 @@ function RootLayoutContent({ children }: { children: ReactNode }) {
<ThemeProvider>
<DeepLinkProvider>
<SidebarProvider defaultOpen={false}>
<TitleBar />
<TitleBar onOpenCommandPalette={() => openCommandPalette("")} />
<AppSidebar />
<div className="flex h-screenish min-w-0 flex-1 flex-col overflow-hidden mt-[var(--layout-title-bar-offset)] border-l border-border bg-background">
<SubscriptionStatusBanner />
Expand All @@ -212,6 +255,12 @@ function RootLayoutContent({ children }: { children: ReactNode }) {
/>
<ReleaseNotesDialog />
<ForceCloseDialog />
<CommandPalette
open={isCommandPaletteOpen}
query={commandPaletteQuery}
onOpenChange={handleCommandPaletteOpenChange}
onQueryChange={setCommandPaletteQuery}
/>
</SidebarProvider>
</DeepLinkProvider>
</ThemeProvider>
Expand Down
14 changes: 0 additions & 14 deletions src/components/AppSearchDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@ type AppSearchDialogProps = {
onOpenChange: (open: boolean) => void;
onSelectApp: (appId: number) => void;
allApps: AppSearchResult[];
disableShortcut?: boolean;
};

export function AppSearchDialog({
open,
onOpenChange,
onSelectApp,
allApps,
disableShortcut,
}: AppSearchDialogProps) {
const [searchQuery, setSearchQuery] = useState<string>("");
function useDebouncedValue<T>(value: T, delay: number): T {
Expand Down Expand Up @@ -88,18 +86,6 @@ export function AppSearchDialog({
return { before, match, after, raw: before + match + after };
}

useEffect(() => {
if (disableShortcut) return;
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
onOpenChange(!open);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, [open, onOpenChange, disableShortcut]);

return (
<CommandDialog
open={open}
Expand Down
11 changes: 0 additions & 11 deletions src/components/ChatSearchDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,6 @@ export function ChatSearchDialog({
return { before, match, after, raw: before + match + after };
}

useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
onOpenChange(!open);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, [open, onOpenChange]);

return (
<CommandDialog
open={open}
Expand Down
Loading
Loading