diff --git a/packages/app/src/components/command-tooltip-keybind.test.ts b/packages/app/src/components/command-tooltip-keybind.test.ts new file mode 100644 index 000000000000..63c7f32468e1 --- /dev/null +++ b/packages/app/src/components/command-tooltip-keybind.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "bun:test" +import { newTabTooltipKeybind, reviewTooltipKeybind } from "./command-tooltip-keybind" + +describe("command tooltip keybinds", () => { + test("keeps localized review shortcut modifiers", () => { + const command = { + keybind: () => "Ctrl+Maj+R", + keybindParts: () => ["Ctrl", "Maj", "R"], + } + + expect(reviewTooltipKeybind(command, (key) => key)).toEqual(["Ctrl", "Maj", "R"]) + }) + + test("uses the configured new-tab shortcut", () => { + const command = { + keybind: () => "Alt+N", + keybindParts: () => ["Alt", "N"], + } + + expect(newTabTooltipKeybind(command, (key) => key)).toEqual(["Alt", "N"]) + }) +}) diff --git a/packages/app/src/components/command-tooltip-keybind.ts b/packages/app/src/components/command-tooltip-keybind.ts new file mode 100644 index 000000000000..d6685b95a8f0 --- /dev/null +++ b/packages/app/src/components/command-tooltip-keybind.ts @@ -0,0 +1,11 @@ +type CommandKeybind = { + keybindParts: (id: string) => string[] +} + +export function reviewTooltipKeybind(command: CommandKeybind, _translate?: (key: string) => string) { + return command.keybindParts("review.toggle") +} + +export function newTabTooltipKeybind(command: CommandKeybind, _translate?: (key: string) => string) { + return command.keybindParts("tab.new") +} diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 302bf35c3534..20bee2dbb1dc 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -2196,7 +2196,9 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) { )} {props.state.modelName} - + + + } @@ -2225,7 +2227,9 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) { )} {props.state.modelName} - + + + diff --git a/packages/app/src/components/server/server-row-menu.tsx b/packages/app/src/components/server/server-row-menu.tsx index ece38ab49ba2..9d5f5e5a32d4 100644 --- a/packages/app/src/components/server/server-row-menu.tsx +++ b/packages/app/src/components/server/server-row-menu.tsx @@ -19,7 +19,7 @@ export const ServerRowMenu: Component<{ const isDefault = () => props.controller.defaultKey() === key return ( - + view().reviewPanel.toggle(), @@ -520,13 +523,15 @@ type SessionHeaderV2ActionsState = { statusVisible: boolean statusLabel: string reviewLabel: string - reviewKeybind: string + reviewKeybind: string[] reviewVisible: boolean reviewOpened: boolean onReviewToggle: () => void } function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) { + const language = useLanguage() + return (
@@ -535,7 +540,17 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) { - + + {props.state.reviewLabel} + 0}> + + + + } + > } /> - +
) diff --git a/packages/app/src/components/titlebar-session-events.test.ts b/packages/app/src/components/titlebar-session-events.test.ts index e1913e946a41..d73abe944dca 100644 --- a/packages/app/src/components/titlebar-session-events.test.ts +++ b/packages/app/src/components/titlebar-session-events.test.ts @@ -1,15 +1,19 @@ import { describe, expect, test } from "bun:test" +import type { ServerConnection } from "@/context/server" import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "./titlebar-session-events" +const remote = "remote" as ServerConnection.Key + describe("titlebar session events", () => { test("reads valid removed session tab details", () => { expect( readSessionTabsRemovedDetail( new CustomEvent(SESSION_TABS_REMOVED_EVENT, { - detail: { directory: "/tmp/project", sessionIDs: ["ses_1", "ses_2", 1] }, + detail: { server: "remote", directory: "/tmp/project", sessionIDs: ["ses_1", "ses_2", 1] }, }), ), ).toEqual({ + server: remote, directory: "/tmp/project", sessionIDs: ["ses_1", "ses_2"], }) diff --git a/packages/app/src/components/titlebar-session-events.ts b/packages/app/src/components/titlebar-session-events.ts index aa6b5e1d1150..7d914d7c71be 100644 --- a/packages/app/src/components/titlebar-session-events.ts +++ b/packages/app/src/components/titlebar-session-events.ts @@ -1,6 +1,9 @@ +import type { ServerConnection } from "@/context/server" + export const SESSION_TABS_REMOVED_EVENT = "opencode:session-tabs-removed" export type SessionTabsRemovedDetail = { + server?: ServerConnection.Key directory: string sessionIDs: string[] } @@ -18,11 +21,14 @@ export function readSessionTabsRemovedDetail(event: Event): SessionTabsRemovedDe if (!("sessionIDs" in detail)) return undefined if (typeof detail.directory !== "string") return undefined if (!Array.isArray(detail.sessionIDs)) return undefined + if ("server" in detail && detail.server !== undefined && typeof detail.server !== "string") return undefined const sessionIDs = detail.sessionIDs.filter((id): id is string => typeof id === "string") if (sessionIDs.length === 0) return undefined return { + server: + "server" in detail && typeof detail.server === "string" ? (detail.server as ServerConnection.Key) : undefined, directory: detail.directory, sessionIDs, } diff --git a/packages/app/src/components/titlebar-tab-nav.tsx b/packages/app/src/components/titlebar-tab-nav.tsx index aa821678475f..0c08506e1b01 100644 --- a/packages/app/src/components/titlebar-tab-nav.tsx +++ b/packages/app/src/components/titlebar-tab-nav.tsx @@ -175,9 +175,10 @@ export function TabNavItem(props: { forwardTabRef(props.ref, el) }} data-titlebar-tab + data-slot="titlebar-tab-item" data-title-overflow={titleOverflowing()} data-editing={editing()} - class="group relative flex h-7 min-w-24 max-w-60 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]" + class="group relative flex h-7 min-w-24 max-w-60 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]" classList={{ invisible: props.hidden }} data-active={props.active} data-dragging={props.dragging} @@ -292,10 +293,11 @@ export function DraftTabItem(props: {
forwardTabRef(props.ref, el)} data-titlebar-tab + data-slot="titlebar-tab-item" data-active={props.active} data-dragging={props.dragging} data-pressed={props.pressed} - class="group relative shrink-0 flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] pl-1.5 pr-8 whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-overlay-simple-overlay-pressed)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]" + class="group relative shrink-0 flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] pl-1.5 pr-8 whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-overlay-simple-overlay-pressed)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)]" classList={{ invisible: props.hidden }} onMouseDown={(event) => { if (event.button !== 1) return diff --git a/packages/app/src/components/titlebar.css b/packages/app/src/components/titlebar.css index edce93c3106a..348e1c443dd2 100644 --- a/packages/app/src/components/titlebar.css +++ b/packages/app/src/components/titlebar.css @@ -1,3 +1,29 @@ +[data-slot="titlebar-tab-item"] { + user-select: none; +} + +[data-slot="titlebar-tab-item"] a { + outline: none; +} + +[data-slot="titlebar-v2"] [data-component="icon-button-v2"][data-variant="ghost-muted"]:is(:focus-visible, [data-state="focus"]):not(:disabled) { + outline: none; + background-color: var(--v2-overlay-simple-overlay-hover); +} + +[data-slot="titlebar-tab-item"] [data-component="icon-button-v2"]:is(:focus-visible, [data-state="focus"]):not(:disabled) { + opacity: 1; +} + +[data-slot="titlebar-tab-item"]:has([data-component="icon-button-v2"]:focus-visible) [data-slot="titlebar-tab-close"] { + right: 0; + left: auto; +} + +[data-slot="titlebar-tab-item"]:has([data-component="icon-button-v2"]:focus-visible) [data-slot="titlebar-tab-close-fade"] { + background-image: var(--active-bg); +} + @keyframes titlebar-tab-fade-left { from { visibility: hidden; diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index e3660bc73c73..431b5445bc41 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -26,6 +26,7 @@ import { useGlobal } from "@/context/global" import { ServerConnection, useServer } from "@/context/server" import { tabKey, useTabs } from "@/context/tabs" import "./titlebar.css" +import { newTabTooltipKeybind } from "./command-tooltip-keybind" type TauriDesktopWindow = { startDragging?: () => Promise @@ -217,6 +218,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { return (
tabsStoreActions.reorder(keys)} /> - } - onClick={openNewTab} - aria-label={language.t("command.session.new")} - /> + + {language.t("command.session.new")} + + + } + > + } + onClick={openNewTab} + aria-label={language.t("command.session.new")} + /> +
@@ -663,16 +675,16 @@ function TitlebarV2Right(props: { state: TitlebarV2RightState }) { function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) { return ( -
+
setState("menuOpen", open)} /> - } - aria-label={props.language.t("home.project.add")} - onClick={() => props.chooseProject(props.server)} - /> + + } + aria-label={props.language.t("home.project.add")} + onClick={() => props.chooseProject(props.server)} + /> +
) @@ -733,19 +786,11 @@ function HomeProjectRow(props: { {displayName(props.project)}
- } - aria-label={props.language.t("command.session.new")} - onClick={() => props.openNewSession(props.server, props.project.worktree)} - /> props.editProject(props.server, props.project)}> - {props.language.t("common.edit")} + {props.language.t("dialog.project.edit.title")} + } + aria-label={props.language.t("command.session.new")} + onClick={() => props.openNewSession(props.server, props.project.worktree)} + />
) @@ -810,7 +863,7 @@ function HomeSessionLeading(props: {