mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
fix(app): open the command palette from anywhere (#57933)
This commit is contained in:
committed by
GitHub
Unverified
parent
f06750a692
commit
4d38fcda8c
@@ -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"),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user