diff --git a/packages/app/e2e/file-tree.spec.ts b/packages/app/e2e/file-tree.spec.ts index c22a810f4f0..b56667049a4 100644 --- a/packages/app/e2e/file-tree.spec.ts +++ b/packages/app/e2e/file-tree.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from "./fixtures" test("file tree can expand folders and open a file", async ({ page, gotoSession }) => { await gotoSession() - const toggle = page.getByRole("button", { name: "Toggle file tree" }) + const toggle = page.getByRole("button", { name: "Toggle review" }) const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]') if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click() diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index 1e37d8f6a20..6295b3012ee 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -1,4 +1,4 @@ -import { Match, Show, Switch, createMemo } from "solid-js" +import { Match, Show, Switch, createMemo, createSignal, onCleanup } from "solid-js" import { Tooltip } from "@opencode-ai/ui/tooltip" import { ProgressCircle } from "@opencode-ai/ui/progress-circle" import { Button } from "@opencode-ai/ui/button" @@ -14,15 +14,22 @@ interface SessionContextUsageProps { variant?: "button" | "indicator" } +const isTouch = () => window.matchMedia("(hover: none)").matches + export function SessionContextUsage(props: SessionContextUsageProps) { const sync = useSync() const params = useParams() const layout = useLayout() const language = useLanguage() + const [touchTooltip, setTouchTooltip] = createSignal(false) + let tooltipTimer: number | undefined + + onCleanup(() => clearTimeout(tooltipTimer)) const variant = createMemo(() => props.variant ?? "button") const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) + const view = createMemo(() => layout.view(sessionKey)) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const usd = createMemo( @@ -57,12 +64,32 @@ export function SessionContextUsage(props: SessionContextUsageProps) { const openContext = () => { if (!params.id) return - layout.fileTree.open() layout.fileTree.setTab("all") + layout.fileTree.open() + view().mobileTab.set("context") tabs().open("context") tabs().setActive("context") } + const handleClick = () => { + if (variant() === "indicator") return + if (isTouch() && !touchTooltip()) { + setTouchTooltip(true) + clearTimeout(tooltipTimer) + tooltipTimer = window.setTimeout(() => setTouchTooltip(false), 3000) + return + } + setTouchTooltip(false) + openContext() + } + + const handleOpenChange = (open: boolean) => { + // On touch devices, we control the tooltip state ourselves via handleClick + // Ignore Kobalte's attempts to close it + if (isTouch()) return + setTouchTooltip(open) + } + const circle = () => (
@@ -94,7 +121,12 @@ export function SessionContextUsage(props: SessionContextUsageProps) { return ( - + {circle()} @@ -102,7 +134,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { type="button" variant="ghost" class="size-6" - onClick={openContext} + onClick={handleClick} aria-label={language.t("context.usage.view")} > {circle()} diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 37733caff63..42f3c80864c 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -367,14 +367,14 @@ export function SessionContextTab(props: SessionContextTabProps) { return (
{ scroll = el restoreScroll() }} onScroll={handleScroll} > -
+
{(stat) => }
diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index d30fd11cfb7..8ffd6398ccd 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -113,6 +113,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( mobileSidebar: { opened: false, }, + mobileTab: "session" as "session" | "changes" | "context", sessionTabs: {} as Record, sessionView: {} as Record, }), @@ -604,6 +605,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setStore("sessionView", session, "reviewOpen", open) }, }, + mobileTab: { + value: createMemo(() => store.mobileTab), + set(tab: "session" | "changes" | "context") { + setStore("mobileTab", tab) + }, + }, } }, tabs(sessionKey: string | Accessor) { diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 87ebf6db72e..be12ec2f19a 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -435,7 +435,6 @@ export default function Page() { expanded: {} as Record, messageId: undefined as string | undefined, turnStart: 0, - mobileTab: "session" as "session" | "changes", newSessionWorktree: "main", promptHeight: 0, }) @@ -1104,7 +1103,8 @@ export default function Page() { .filter((tab) => tab !== "context"), ) - const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") + const mobileChanges = createMemo(() => !isDesktop() && view().mobileTab.value() === "changes") + const mobileContext = createMemo(() => !isDesktop() && view().mobileTab.value() === "context") const fileTreeTab = () => layout.fileTree.tab() const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value) @@ -1292,7 +1292,9 @@ export default function Page() { const id = params.id if (!id) return - const wants = isDesktop() ? layout.fileTree.opened() && fileTreeTab() === "changes" : store.mobileTab === "changes" + const wants = isDesktop() + ? layout.fileTree.opened() && fileTreeTab() === "changes" + : view().mobileTab.value() === "changes" if (!wants) return if (sync.data.session_diff[id] !== undefined) return if (sync.status === "loading") return @@ -1728,24 +1730,18 @@ export default function Page() {
- {/* Mobile tab bar */} + {/* Mobile tab bar - only shown on mobile when there's a session */} - + view().mobileTab.set(value as "session" | "changes" | "context")} + > - setStore("mobileTab", "session")} - > + {language.t("session.tab.session")} - setStore("mobileTab", "changes")} - > + {language.t("session.review.filesChanged", { count: reviewCount() })} @@ -1753,6 +1749,9 @@ export default function Page() { {language.t("session.review.change.other")} + + {language.t("session.tab.context")} + @@ -1773,9 +1772,8 @@ export default function Page() { - +
@@ -1820,8 +1818,18 @@ export default function Page() {
- } - > +
+ +
+ +
+
+
-
+
+
diff --git a/packages/ui/src/components/tooltip.tsx b/packages/ui/src/components/tooltip.tsx index 37517fb346d..5d4d588f56d 100644 --- a/packages/ui/src/components/tooltip.tsx +++ b/packages/ui/src/components/tooltip.tsx @@ -9,6 +9,8 @@ export interface TooltipProps extends ComponentProps { contentStyle?: JSX.CSSProperties inactive?: boolean forceOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void } export interface TooltipKeybindProps extends Omit { @@ -32,7 +34,7 @@ export function TooltipKeybind(props: TooltipKeybindProps) { } export function Tooltip(props: TooltipProps) { - const [open, setOpen] = createSignal(false) + const [internalOpen, setInternalOpen] = createSignal(false) const [local, others] = splitProps(props, [ "children", "class", @@ -40,8 +42,19 @@ export function Tooltip(props: TooltipProps) { "contentStyle", "inactive", "forceOpen", + "open", + "onOpenChange", ]) + const open = () => local.open ?? internalOpen() + const setOpen = (value: boolean) => { + if (local.onOpenChange) { + local.onOpenChange(value) + } else { + setInternalOpen(value) + } + } + const c = children(() => local.children) onMount(() => {