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 ( - + - + Update diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 16483706b8e9..0c935307ade1 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -227,6 +227,11 @@ export function formatKeybind(config: string, t?: (key: KeyLabel) => string): st return IS_MAC ? parts.join("") : parts.join("+") } +// KeybindV2 takes an array instead of a string +export function formatKeybindKeys(config: string, t?: (key: KeyLabel) => string): string[] { + return formatKeybindParts(config, t) +} + function isEditableTarget(target: EventTarget | null) { if (!(target instanceof HTMLElement)) return false if (target.isContentEditable) return true diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx index 3f559e889885..a7e9777f005c 100644 --- a/packages/app/src/context/tabs.tsx +++ b/packages/app/src/context/tabs.tsx @@ -205,9 +205,10 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ if (server.key === key) navigate("/") }, removeSessions: (input: SessionTabsRemovedDetail) => { + const targetServer = input.server ?? server.key const removed = store .filter( - (tab) => tab.type === "session" && tab.server === server.key && input.sessionIDs.includes(tab.sessionId), + (tab) => tab.type === "session" && tab.server === targetServer && input.sessionIDs.includes(tab.sessionId), ) .map(tabKey) void startTransition(() => { @@ -215,28 +216,28 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ produce((tabs) => { const sessionIDs = new Set(input.sessionIDs) const currentHref = - params.dir && params.id + targetServer === server.key && params.dir && params.id ? tabHref({ type: "session", - server: server.key, + server: targetServer, sessionId: params.id, }) : undefined const currentIndex = currentHref ? tabs.findIndex( - (tab) => tab.type === "session" && tab.server === server.key && tabHref(tab) === currentHref, + (tab) => tab.type === "session" && tab.server === targetServer && tabHref(tab) === currentHref, ) : -1 const currentTab = tabs[currentIndex] const removedCurrent = currentTab?.type === "session" && - currentTab.server === server.key && + currentTab.server === targetServer && sessionIDs.has(currentTab.sessionId) for (let i = tabs.length - 1; i >= 0; i--) { const tab = tabs[i] if (!tab || tab.type !== "session") continue - if (tab.server !== server.key) continue + if (tab.server !== targetServer) continue if (!sessionIDs.has(tab.sessionId)) continue tabs.splice(i, 1) } diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 2257c8a6ce1d..bae4c23d9759 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -592,6 +592,7 @@ export const dict = { "home.server.collapse": "Collapse server projects", "home.server.expand": "Expand server projects", "home.sessions.search.placeholder": "Search sessions", + "home.sessions.search.placeholder.scoped": "Search sessions in {{scope}}", "home.sessions.search.sessions": "Sessions", "home.sessions.search.noResults": "No sessions found for {{query}}", "home.sessions.empty": "No sessions found", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 4e17ad94afb6..913bcd70f275 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -507,6 +507,7 @@ export const dict = { "home.projects": "项目", "home.project.add": "添加项目", "home.sessions.search.placeholder": "搜索会话", + "home.sessions.search.placeholder.scoped": "在 {{scope}} 中搜索会话", "home.sessions.search.sessions": "会话", "home.sessions.search.noResults": "未找到与 {{query}} 相关的会话", "home.sessions.empty": "未找到会话", diff --git a/packages/app/src/pages/home-session-archive.test.ts b/packages/app/src/pages/home-session-archive.test.ts new file mode 100644 index 000000000000..0ad30afcd250 --- /dev/null +++ b/packages/app/src/pages/home-session-archive.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from "bun:test" +import { SESSION_TABS_REMOVED_EVENT, readSessionTabsRemovedDetail } from "@/components/titlebar-session-events" +import { archiveHomeSession } from "./home-session-archive" +import type { ServerConnection } from "@/context/server" + +const remote = "remote" as ServerConnection.Key + +test("archiving a Home session removes its open titlebar tab", async () => { + let detail: ReturnType + let removed = false + window.addEventListener( + SESSION_TABS_REMOVED_EVENT, + (event) => { + detail = readSessionTabsRemovedDetail(event) + }, + { once: true }, + ) + + await archiveHomeSession({ + server: remote, + session: { id: "ses_1", directory: "/workspace" }, + update: async () => undefined, + remove: () => { + removed = true + }, + }) + + expect(removed).toBe(true) + expect(detail).toEqual({ server: remote, directory: "/workspace", sessionIDs: ["ses_1"] }) +}) + +test("reports archive failures without removing the session", async () => { + const failure = new Error("offline") + let error: unknown + let removed = false + + await archiveHomeSession({ + server: remote, + session: { id: "ses_1", directory: "/workspace" }, + update: async () => Promise.reject(failure), + remove: () => { + removed = true + }, + onError: (value) => { + error = value + }, + }) + + expect(error).toBe(failure) + expect(removed).toBe(false) +}) diff --git a/packages/app/src/pages/home-session-archive.ts b/packages/app/src/pages/home-session-archive.ts new file mode 100644 index 000000000000..7e6634ed7ab7 --- /dev/null +++ b/packages/app/src/pages/home-session-archive.ts @@ -0,0 +1,37 @@ +import { notifySessionTabsRemoved } from "@/components/titlebar-session-events" +import type { ServerConnection } from "@/context/server" + +type HomeSession = { + id: string + directory: string +} + +type SessionUpdate = { + directory: string + sessionID: string + time: { archived: number } +} + +export async function archiveHomeSession(input: { + server: ServerConnection.Key + session: HomeSession + update: (value: SessionUpdate) => Promise + remove: () => void + onError?: (error: unknown) => void +}) { + await input + .update({ + directory: input.session.directory, + sessionID: input.session.id, + time: { archived: Date.now() }, + }) + .then(() => { + input.remove() + notifySessionTabsRemoved({ + server: input.server, + directory: input.session.directory, + sessionIDs: [input.session.id], + }) + }) + .catch((error) => input.onError?.(error)) +} diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index e46e5ad4cb37..37ecd05f2f94 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -14,7 +14,7 @@ import { Switch, } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" -import { createStore } from "solid-js/store" +import { createStore, produce } from "solid-js/store" import { useQuery } from "@tanstack/solid-query" import { Button } from "@opencode-ai/ui/button" import { Logo } from "@opencode-ai/ui/logo" @@ -25,6 +25,7 @@ import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { getProjectAvatarVariant, useLayout, type LocalProject } from "@/context/layout" import { useNavigate } from "@solidjs/router" import { base64Encode } from "@opencode-ai/core/util/encode" @@ -35,7 +36,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDirectoryPicker } from "@/components/directory-picker" import { DialogSelectServer, useServerManagementController } from "@/components/dialog-select-server" import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2" -import { ServerConnection, useServer } from "@/context/server" +import { ServerConnection, serverName, useServer } from "@/context/server" import { sessionHasOpenTab, useTabs } from "@/context/tabs" import { useServerSync, type ServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" @@ -43,6 +44,7 @@ import { useNotification } from "@/context/notification" import { closeHomeProject, displayName, + errorMessage, getProjectAvatarSource, homeProjectDirectories, type HomeProjectSelection, @@ -55,12 +57,15 @@ import { sessionTitle } from "@/utils/session-title" import { pathKey } from "@/utils/path-key" import { useGlobal } from "@/context/global" import { useCommand } from "@/context/command" +import { Binary } from "@opencode-ai/core/util/binary" import { ServerRowMenu } from "@/components/server/server-row-menu" import { ServerHealthIndicator } from "@/components/server/server-row" import { type ServerHealth } from "@/utils/server-health" import { Persist, persisted } from "@/utils/persist" import { useMarked } from "@opencode-ai/ui/context/marked" import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache" +import { archiveHomeSession } from "./home-session-archive" +import { showToast } from "@/utils/toast" const HOME_SESSION_LIMIT = 64 const HOME_ROW_LAYOUT = @@ -85,7 +90,7 @@ type HomeSessionGroup = { const HOME_SESSION_SEARCH_RESULTS_ID = "home-session-search-results" const HOME_SEARCH_RESULT_ROW = - "flex h-10 w-full shrink-0 cursor-default items-center gap-2 border-0 py-3 pl-4 pr-6 text-left transition-[background-color] duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none" + "flex h-10 w-full shrink-0 cursor-default items-center gap-2 border-0 py-3 pl-[18px] pr-6 text-left transition-[background-color] duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none" const HOME_SEARCH_RESULT_TITLE = "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-base [font-weight:530]" const HOME_SEARCH_RESULT_META = @@ -172,6 +177,19 @@ export function NewHome() { return directories(project) }) const search = createMemo(() => state.search.trim()) + const searchPlaceholder = createMemo(() => { + const project = selectedProject() + if (project) { + return language.t("home.sessions.search.placeholder.scoped", { scope: displayName(project) }) + } + if (global.servers.list().length > 1) { + const conn = focusedServer() + if (conn) { + return language.t("home.sessions.search.placeholder.scoped", { scope: serverName(conn) }) + } + } + return language.t("home.sessions.search.placeholder") + }) const sessionLoad = useQuery(() => ({ queryKey: ["home", "sessions", state.selection.server, ...projectDirectories()] as const, queryFn: async () => { @@ -258,7 +276,7 @@ export function NewHome() { command.register("home", () => [ { id: "home.sessions.search.focus", - title: language.t("home.sessions.search.placeholder"), + title: searchPlaceholder(), keybind: "mod+f", hidden: true, onSelect: () => focusSessionSearch?.(), @@ -350,6 +368,30 @@ export function NewHome() { }) } + async function archiveSession(session: Session) { + const conn = focusedServer() + const ctx = focusedServerCtx() + if (!conn || !ctx) return + const [, setStore] = ctx.sync.child(session.directory) + await archiveHomeSession({ + server: ServerConnection.key(conn), + session, + update: (value) => ctx.sdk.client.session.update(value), + remove: () => + setStore( + produce((draft) => { + const match = Binary.search(draft.session, session.id, (s) => s.id) + if (match.found) draft.session.splice(match.index, 1) + }), + ), + onError: (error) => + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(error, language.t("common.requestFailed")), + }), + }) + } + function chooseProject(conn: ServerConnection.Any) { function resolve(result: string | string[] | null) { addProjects(conn, homeProjectDirectories(result)) @@ -402,10 +444,11 @@ export function NewHome() { > ( openSession(record.session)} + openSession={openSession} + archiveSession={archiveSession} /> )} @@ -502,15 +547,17 @@ function HomeProjectColumn(props: { {props.language.t("home.projects")} - } - onClick={() => props.chooseProject(global.servers.list()[0]!)} - aria-label={props.language.t("home.project.add")} - /> + + } + onClick={() => props.chooseProject(global.servers.list()[0]!)} + aria-label={props.language.t("home.project.add")} + /> + 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: { + - + 0} fallback={ - + {props.noResultsLabel} } > - + {language.t("home.sessions.search.sessions")} - - - {(record) => ( - setStore("active", homeSessionSearchKey(record))} - onSelect={(session) => props.onSelect(session)} - /> - )} - - + (listRef = el)}> + + + {(record) => ( + setStore("active", homeSessionSearchKey(record))} + onSelect={(session) => props.onSelect(session)} + /> + )} + + + @@ -962,11 +1019,10 @@ function HomeSessionSearch(props: { @@ -1031,6 +1087,7 @@ function HomeSessionSearch(props: { function HomeSessionSearchResultRow(props: { record: HomeSessionRecord + showProjectName: boolean server: ServerConnection.Key activeServer: boolean selected: boolean @@ -1038,6 +1095,7 @@ function HomeSessionSearchResultRow(props: { onSelect: (session: Session) => void }) { const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) + const showProjectName = () => props.showProjectName && props.record.projectName const key = () => homeSessionSearchKey(props.record) @@ -1064,11 +1122,11 @@ function HomeSessionSearchResultRow(props: { /> {title()} - + {props.record.projectName} @@ -1079,7 +1137,7 @@ function HomeSessionSearchResultRow(props: { function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => void }) { const language = useLanguage() return ( - + {props.title} {(onNewSession) => ( @@ -1101,36 +1159,58 @@ function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => voi function HomeSessionRow(props: { record: HomeSessionRecord + showProjectName: boolean server: ServerConnection.Key activeServer: boolean - onClick: () => void + openSession: (session: Session) => void + archiveSession: (session: Session) => Promise }) { + const language = useLanguage() const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) + const showProjectName = () => props.showProjectName && props.record.projectName return ( - - - + props.openSession(props.record.session)} > - {title()} - - - - {props.record.projectName} + + + {title()} - - + + + {props.record.projectName} + + + + + + } + aria-label={language.t("common.archive")} + onClick={(event) => { + event.preventDefault() + event.stopPropagation() + void props.archiveSession(props.record.session) + }} + /> + + + ) } diff --git a/packages/ui/src/components/dialog.css b/packages/ui/src/components/dialog.css index 1e74763ae2d8..f17232c3507d 100644 --- a/packages/ui/src/components/dialog.css +++ b/packages/ui/src/components/dialog.css @@ -130,8 +130,8 @@ } &[data-size="x-large"] [data-slot="dialog-container"] { - width: min(calc(100vw - 32px), 960px); - height: min(calc(100vh - 32px), 600px); + width: min(calc(100vw - 32px), 980px); + height: min(calc(100vh - 92px), 600px); } } diff --git a/packages/ui/src/components/scroll-view.css b/packages/ui/src/components/scroll-view.css index f6a49e241c6e..7c5d8ed0f7bb 100644 --- a/packages/ui/src/components/scroll-view.css +++ b/packages/ui/src/components/scroll-view.css @@ -1,10 +1,14 @@ .scroll-view { position: relative; + display: flex; + flex-direction: column; + min-height: 0; overflow: hidden; } .scroll-view__viewport { - height: 100%; + flex: 1 1 auto; + min-height: 0; width: 100%; overflow-y: auto; scrollbar-width: none; diff --git a/packages/ui/src/theme/themes/oc-2.json b/packages/ui/src/theme/themes/oc-2.json index 91c10f12d6b0..07535179cb43 100644 --- a/packages/ui/src/theme/themes/oc-2.json +++ b/packages/ui/src/theme/themes/oc-2.json @@ -416,7 +416,7 @@ "v2-overlay-simple-overlay-pressed": "var(--v2-alpha-light-10)", "v2-overlay-simple-overlay-contrast-hover": "var(--v2-alpha-dark-24)", "v2-overlay-simple-overlay-contrast-pressed": "var(--v2-alpha-dark-40)", - "v2-overlay-simple-overlay-scrim": "var(--v2-alpha-light-30)", + "v2-overlay-simple-overlay-scrim": "var(--v2-alpha-dark-60)", "v2-overlay-gradient-depth-overlay-depth-top": "var(--v2-alpha-light-100)", "v2-overlay-gradient-depth-overlay-depth-bot": "var(--v2-alpha-light-0)", "v2-overlay-simple-tab-active-scrim": "#24242400", diff --git a/packages/ui/src/theme/v2/mapping.ts b/packages/ui/src/theme/v2/mapping.ts index af9306aaca48..cf44d5e1cce7 100644 --- a/packages/ui/src/theme/v2/mapping.ts +++ b/packages/ui/src/theme/v2/mapping.ts @@ -90,7 +90,7 @@ const dark: Record = { "v2-overlay-simple-overlay-pressed": ref("v2-alpha-light-10"), "v2-overlay-simple-overlay-contrast-hover": ref("v2-alpha-dark-24"), "v2-overlay-simple-overlay-contrast-pressed": ref("v2-alpha-dark-40"), - "v2-overlay-simple-overlay-scrim": ref("v2-alpha-light-30"), + "v2-overlay-simple-overlay-scrim": ref("v2-alpha-dark-60"), "v2-overlay-gradient-depth-overlay-depth-top": ref("v2-alpha-light-100"), "v2-overlay-gradient-depth-overlay-depth-bot": ref("v2-alpha-light-0"), "v2-overlay-simple-tab-active-scrim": "#24242400", diff --git a/packages/ui/src/v2/components/dialog-v2.css b/packages/ui/src/v2/components/dialog-v2.css index fa18dfc92e50..c5c84739c946 100644 --- a/packages/ui/src/v2/components/dialog-v2.css +++ b/packages/ui/src/v2/components/dialog-v2.css @@ -144,7 +144,7 @@ } &[data-size="x-large"] [data-slot="dialog-container"] { - width: 800px; - height: 560px; + width: min(calc(100vw - 32px), 980px); + height: min(calc(100vh - 92px), 600px); } } diff --git a/packages/ui/src/v2/components/icon-button-v2.css b/packages/ui/src/v2/components/icon-button-v2.css index 88e15ffd6fd9..ce36eeb61730 100644 --- a/packages/ui/src/v2/components/icon-button-v2.css +++ b/packages/ui/src/v2/components/icon-button-v2.css @@ -112,9 +112,7 @@ color: var(--v2-text-text-base); } -[data-component="icon-button-v2"][data-variant="ghost"]:is(:hover, [data-state="hover"]):not(:disabled):not( - [data-expanded] - ) { +[data-component="icon-button-v2"][data-variant="ghost"]:is(:hover, [data-state="hover"], [data-expanded]):not(:disabled) { background-color: var(--v2-overlay-simple-overlay-hover); } @@ -133,9 +131,7 @@ color: var(--v2-icon-icon-muted); } -[data-component="icon-button-v2"][data-variant="ghost-muted"]:is(:hover, [data-state="hover"]):not(:disabled):not( - [data-expanded] - ) { +[data-component="icon-button-v2"][data-variant="ghost-muted"]:is(:hover, [data-state="hover"], [data-expanded]):not(:disabled) { background-color: var(--v2-overlay-simple-overlay-hover); } diff --git a/packages/ui/src/v2/components/icon.tsx b/packages/ui/src/v2/components/icon.tsx index 60202a85c8a7..316ca7788831 100644 --- a/packages/ui/src/v2/components/icon.tsx +++ b/packages/ui/src/v2/components/icon.tsx @@ -65,6 +65,10 @@ const icons = { viewBox: "0 0 16 16", body: ``, }, + archive: { + viewBox: "0 0 16 16", + body: ``, + }, } const spriteID = "opencode-v2-icon-sprite" diff --git a/packages/ui/src/v2/components/project-avatar-v2.css b/packages/ui/src/v2/components/project-avatar-v2.css index c99bc4a1183b..d7ec963994b8 100644 --- a/packages/ui/src/v2/components/project-avatar-v2.css +++ b/packages/ui/src/v2/components/project-avatar-v2.css @@ -120,7 +120,7 @@ flex-shrink: 0; align-items: center; justify-content: center; - width: 22px; - height: 22px; + width: 16px; + height: 16px; overflow: visible; } diff --git a/packages/ui/src/v2/components/select-v2.css b/packages/ui/src/v2/components/select-v2.css index 63fd103676b5..38562b2ea6ad 100644 --- a/packages/ui/src/v2/components/select-v2.css +++ b/packages/ui/src/v2/components/select-v2.css @@ -134,7 +134,7 @@ font-style: normal; font-weight: 440; font-size: 13px; - line-height: 1; + line-height: 16px; letter-spacing: -0.04px; color: var(--v2-text-text-base); font-variation-settings: "slnt" 0; diff --git a/packages/ui/src/v2/components/tabs-v2.css b/packages/ui/src/v2/components/tabs-v2.css index f88ba67ae942..8582bbf678c0 100644 --- a/packages/ui/src/v2/components/tabs-v2.css +++ b/packages/ui/src/v2/components/tabs-v2.css @@ -45,6 +45,7 @@ align-items: center; flex-shrink: 0; white-space: nowrap; + user-select: none; } [data-component="tabs-v2"] [data-slot="tabs-v2-trigger"] { @@ -54,6 +55,7 @@ overflow: hidden; text-overflow: ellipsis; outline: none; + user-select: none; font-size: 13px; font-weight: 440; @@ -61,7 +63,7 @@ letter-spacing: -0.04px; } -[data-component="tabs-v2"] [data-slot="tabs-v2-trigger"]:focus-visible { +[data-component="tabs-v2"] [data-slot="tabs-v2-trigger"]:is(:focus, :focus-visible) { outline: none; box-shadow: none; } diff --git a/packages/ui/src/v2/styles/theme.css b/packages/ui/src/v2/styles/theme.css index f0236f83f918..c704fde6c3ea 100644 --- a/packages/ui/src/v2/styles/theme.css +++ b/packages/ui/src/v2/styles/theme.css @@ -370,7 +370,7 @@ --v2-overlay-simple-overlay-pressed: var(--v2-alpha-light-10); --v2-overlay-simple-overlay-contrast-hover: var(--v2-alpha-dark-24); --v2-overlay-simple-overlay-contrast-pressed: var(--v2-alpha-dark-40); - --v2-overlay-simple-overlay-scrim: var(--v2-alpha-light-30); + --v2-overlay-simple-overlay-scrim: var(--v2-alpha-dark-60); --v2-overlay-gradient-depth-overlay-depth-top: var(--v2-alpha-light-100); --v2-overlay-gradient-depth-overlay-depth-bot: var(--v2-alpha-light-0); --v2-overlay-simple-tab-active-scrim: #24242400;
+
{props.noResultsLabel}
{language.t("home.sessions.search.sessions")}