fix(app): open the command palette from anywhere (#57933)

This commit is contained in:
Kit Langton
2026-10-21 15:32:39 -07:00
committed by GitHub
Unverified
parent f06750a692
commit 4d38fcda8c
6 changed files with 72 additions and 48 deletions
@@ -79,6 +79,27 @@ test("finds a setting and opens Settings with its row in view and highlighted",
await expect(row).toBeInViewport()
})
test("opens from the Settings page and finds another setting", async ({ page }) => {
await openPaletteWorkspace(page, true)
await page.keyboard.press("ControlOrMeta+,")
await expect(page).toHaveURL("/settings")
const screen = page.getByTestId("settings-screen")
await expect(screen.getByRole("tab", { name: "Preferences", exact: true })).toBeVisible()
await page.keyboard.press("ControlOrMeta+K")
const dialog = page.getByRole("dialog")
const input = dialog.getByRole("combobox")
await expect(input).toBeFocused()
await input.fill("theme")
await expect(dialog.getByRole("option", { name: /^Theme/ })).toHaveAttribute("aria-selected", "true")
await input.press("Enter")
await expect(dialog).toHaveCount(0)
await expect(page).toHaveURL("/settings?tab=appearance")
const row = screen.locator('[data-search-target="row"]')
await expect(row).toContainText("Theme")
await expect(row).toBeInViewport()
})
test("lists recent sessions before a query is entered", async ({ page }) => {
const { dialog } = await openCommandPalette(page, true)
const session = dialog.getByRole("option", { name: /Palette fixture session/ })
@@ -150,7 +150,7 @@ export function createHomeSessionsController(home: HomeController) {
})
})
command.register("home.palette", () => [
command.register("palette", () => [
{
id: "command.palette",
title: language.t("command.palette"),
+9 -44
View File
@@ -11,8 +11,7 @@ import {
} from "@opencode/ui/project-avatar"
import { TextShimmer } from "@opencode/ui/text-shimmer"
import { Tooltip } from "@opencode/ui/tooltip"
import { useDialog } from "@opencode/ui/context/dialog"
import { createEffect, createMemo, For, Match, Show, startTransition, Switch, type JSX } from "solid-js"
import { createEffect, createMemo, For, Match, Show, Switch, type JSX } from "solid-js"
import { useLanguage } from "@/runtime/i18n/language"
import { useGlobal } from "@/runtime/server/runtime"
import { ServerConnection, useServers } from "@/runtime/server/registry"
@@ -20,7 +19,6 @@ import { useSettingsCommand } from "@/settings/command"
import { useSettingsSurface } from "@/settings/surface"
import { usePlatform } from "@/runtime/platform/platform"
import { useCommand } from "@/shell/commands/command"
import { useTabs } from "@/shell/tabs/tabs"
import { SessionTabAvatarView } from "@/shell/layout/session-tab-avatar"
import { sessionTabTitle } from "@/shell/titlebar/tab-title"
import type { InboxEntry } from "@/shell/titlebar/inbox-model"
@@ -31,7 +29,8 @@ type Language = ReturnType<typeof useLanguage>
/** Home: a greeting, every session sorted by what it needs from you, and recent projects beside them. */
export function StartHome() {
const model = createStartModel()
useStartCommands()
// Settings opens from Home like from a session; the palette is the shell's.
useSettingsCommand()
return (
<div
@@ -56,45 +55,6 @@ export function StartHome() {
)
}
/** Home's own palette lists commands and this computer's sessions; Settings opens from Home like from a session. */
function useStartCommands() {
const command = useCommand()
const dialog = useDialog()
const global = useGlobal()
const language = useLanguage()
const servers = useServers()
const tabs = useTabs()
useSettingsCommand()
command.register("home.palette", () => [
{
id: "command.palette",
title: language.t("command.palette"),
hidden: true,
onSelect: async () => {
const conn = servers.visible.find((item) => servers.current(item)) ?? servers.visible[0]
if (!conn) return
const { HomeCommandPalette } = await import("./command-palette")
void dialog.show(() => (
<HomeCommandPalette
server={conn}
onSelectSession={(entry) => {
if (!entry.sessionID || !entry.directory || !entry.server) return
const sessionID = entry.sessionID
const server = entry.server
global.ensureServerCtx(conn).projects.touch(entry.project?.worktree ?? entry.directory)
void startTransition(() => {
tabs.select(tabs.addSessionTab({ server, sessionId: sessionID }))
})
}}
/>
))
},
},
])
}
function StartGreeting(props: { model: StartModel }) {
const language = useLanguage()
const command = useCommand()
@@ -336,12 +296,14 @@ function StartProjects(props: { model: StartModel }) {
function ProjectList(props: { model: StartModel; server: StartServer }) {
const language = useLanguage()
// Projects that share a name, like a repository and its worktrees, are told apart by their parent folder.
const repeated = createMemo(() => {
const names = props.server.projects.map((item) => displayName(item.project))
return new Set(names.filter((name, index) => names.indexOf(name) !== index))
})
const parent = (worktree: string) =>
worktree
.replace(/[/\\]+$/, "")
@@ -596,7 +558,10 @@ function Status(props: { model: StartModel; entry: InboxEntry }) {
"text-v2-text-text-muted": status() === "done",
}}
>
{language.t(`sidebar.inbox.status.${status() as "approval" | "input" | "failed" | "done"}`)}
{
// SAFETY: working and ready matched above, so the remaining statuses each have a label.
language.t(`sidebar.inbox.status.${status() as "approval" | "input" | "failed" | "done"}`)
}
</span>
</Match>
</Switch>
+3 -1
View File
@@ -19,7 +19,7 @@ export function useNewSessionCommands(input: {
const language = useLanguage()
useSettingsCommand()
command.register("new-session", () => [
command.register("palette", () => [
{
id: "command.palette",
title: language.t("command.palette"),
@@ -29,6 +29,8 @@ export function useNewSessionCommands(input: {
void dialog.show(() => <DialogCommandPalette />)
},
},
])
command.register("new-session", () => [
{
id: "input.focus",
title: language.t("command.input.focus"),
+1 -1
View File
@@ -237,7 +237,7 @@ export function createActiveSessionRegion(input: {
return model ? { id: model.id, providerID: model.provider.id, variant: selection.variant.current() } : undefined
},
})
command.register("session-palette", () => [
command.register("palette", () => [
{
id: "command.palette",
title: language.t("command.palette"),
+37 -1
View File
@@ -1,7 +1,8 @@
import { Show, Suspense, type ParentProps } from "solid-js"
import { Show, startTransition, Suspense, type ParentProps } from "solid-js"
import { createStore } from "solid-js/store"
import { createMediaQuery } from "@solid-primitives/media"
import { ResizeHandle } from "@opencode/ui/resize-handle"
import { useDialog } from "@opencode/ui/context/dialog"
import { Titlebar } from "@/shell/titlebar/titlebar"
import { usePlatform } from "@/runtime/platform/platform"
import { ToastRegion } from "@/shell/notifications/toast"
@@ -13,6 +14,9 @@ import { ExtensionServerCover } from "@/runtime/extension/server-shell"
import { ExtensionSlot } from "@/runtime/extension/render"
import { useCommand } from "@/shell/commands/command"
import { useLanguage } from "@/runtime/i18n/language"
import { useGlobal } from "@/runtime/server/runtime"
import { useServers } from "@/runtime/server/registry"
import { useTabs } from "@/shell/tabs/tabs"
import { CollapsedSidebarToggle, measureSidebarToggles, paintCollapsedSlots } from "@/shell/sidebar-toggle"
import { createPanelMotion, guardClosingFocus, replayPanelToggle } from "@/shell/panel-motion"
import { windowDragEnabled } from "@/session/header/drag-area"
@@ -25,6 +29,10 @@ export default function Layout(props: ParentProps) {
const preferences = useSettings()
const command = useCommand()
const language = useLanguage()
const dialog = useDialog()
const global = useGlobal()
const servers = useServers()
const tabs = useTabs()
const mobile = createMediaQuery("(max-width: 767px)")
const [state, setState] = createStore<{
@@ -80,6 +88,34 @@ export default function Layout(props: ParentProps) {
]
: [],
)
// Every surface has a palette: routes with their own register under the same key, and the newest wins.
command.register("palette", () => [
{
id: "command.palette",
title: language.t("command.palette"),
hidden: true,
onSelect: async () => {
const conn = servers.visible.find((item) => servers.current(item)) ?? servers.visible[0]
if (!conn) return
const { HomeCommandPalette } = await import("@/home/start/command-palette")
void dialog.show(() => (
<HomeCommandPalette
server={conn}
onSelectSession={(entry) => {
if (!entry.sessionID || !entry.directory || !entry.server) return
const sessionID = entry.sessionID
const server = entry.server
global.ensureServerCtx(conn).projects.touch(entry.project?.worktree ?? entry.directory)
void startTransition(() => {
tabs.select(tabs.addSessionTab({ server, sessionId: sessionID }))
})
}}
/>
))
},
},
])
const bottomTitlebar = () => mobile() && preferences.general.mobileTitlebarPosition() === "bottom"
const dragInset = () => windowDragEnabled(platform) && !mobile()