fix(desktop): context menu button / tab intermittent issue (#34420)

Co-authored-by: Brendan Allan <git@brendonovich.dev>
This commit is contained in:
usrnk1 2026-06-30 11:09:38 +02:00 committed by GitHub
commit 003c22b4a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 71 additions and 24 deletions

View file

@ -26,7 +26,7 @@ function openSessionContext(args: {
layout: ReturnType<typeof useLayout> layout: ReturnType<typeof useLayout>
tabs: ReturnType<ReturnType<typeof useLayout>["tabs"]> tabs: ReturnType<ReturnType<typeof useLayout>["tabs"]>
}) { }) {
if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open() args.view.reviewPanel.open(args.view.reviewPanel.opened() ? "other" : "context-button")
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all") if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
void args.tabs.open("context") void args.tabs.open("context")
args.tabs.setActive("context") args.tabs.setActive("context")
@ -64,16 +64,21 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const cost = createMemo(() => { const cost = createMemo(() => {
return usd().format(info()?.cost ?? 0) return usd().format(info()?.cost ?? 0)
}) })
const contextVisible = createMemo(() => view().reviewPanel.opened() && tabState.activeTab() === "context")
const hasOtherTabs = createMemo(() => tabs().all().some((tab) => tab !== "context" && tab !== "review"))
const openContext = () => { const openContext = () => {
if (!params.id) return if (!params.id) return
if (tabState.activeTab() === "context") { const sessionView = view()
if (contextVisible()) {
tabs().close("context") tabs().close("context")
if (sessionView.reviewPanel.source() === "context-button" && !hasOtherTabs()) sessionView.reviewPanel.close()
return return
} }
openSessionContext({ openSessionContext({
view: view(), view: sessionView,
layout, layout,
tabs: tabs(), tabs: tabs(),
}) })
@ -81,7 +86,20 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const circle = () => ( const circle = () => (
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.usage ?? 0} /> <ProgressCircle
size={16}
strokeWidth={2}
percentage={context()?.usage ?? 0}
style={
variant() === "indicator"
? {
"--progress-circle-background": "var(--v2-background-bg-layer-04, var(--border-weak-base))",
"--progress-circle-background-overlay": "var(--v2-overlay-simple-overlay-pressed, transparent)",
"--progress-circle-progress": "var(--v2-icon-icon-base, var(--icon-base))",
}
: undefined
}
/>
</div> </div>
) )
const circleV2 = () => ( const circleV2 = () => (
@ -119,10 +137,10 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
return ( return (
<Show when={params.id}> <Show when={params.id}>
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}> <Switch>
<Switch> <Match when={variant() === "indicator"}>{circle()}</Match>
<Match when={variant() === "indicator"}>{circle()}</Match> <Match when={buttonAppearance() === "v2"}>
<Match when={buttonAppearance() === "v2"}> <Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
<IconButtonV2 <IconButtonV2
type="button" type="button"
variant="ghost-muted" variant="ghost-muted"
@ -131,8 +149,10 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
onClick={openContext} onClick={openContext}
aria-label={language.t("context.usage.view")} aria-label={language.t("context.usage.view")}
/> />
</Match> </Tooltip>
<Match when={true}> </Match>
<Match when={true}>
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@ -142,9 +162,9 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
> >
{circle()} {circle()}
</Button> </Button>
</Match> </Tooltip>
</Switch> </Match>
</Tooltip> </Switch>
</Show> </Show>
) )
} }

View file

@ -78,6 +78,7 @@ export type LocalProject = Partial<Project> & { worktree: string; expanded: bool
export type HomeProjectSelection = { server: ServerConnection.Key; directory?: string } export type HomeProjectSelection = { server: ServerConnection.Key; directory?: string }
export type ReviewDiffStyle = "unified" | "split" export type ReviewDiffStyle = "unified" | "split"
export type ReviewPanelSource = "context-button" | "other"
export type LayoutRoute = export type LayoutRoute =
| { type: "home" } | { type: "home" }
@ -304,6 +305,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
}), }),
) )
const [ephemeral, setEphemeral] = createStore({
reviewPanelSource: "other" as ReviewPanelSource,
})
const MAX_SESSION_KEYS = 50 const MAX_SESSION_KEYS = 50
const PENDING_MESSAGE_TTL_MS = 2 * 60 * 1000 const PENDING_MESSAGE_TTL_MS = 2 * 60 * 1000
@ -780,6 +784,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} }) const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
const terminalOpened = createMemo(() => store.terminal?.opened ?? false) const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED) const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED)
const reviewPanelSource = createMemo(() => (reviewPanelOpened() ? ephemeral.reviewPanelSource : "other"))
function setTerminalOpened(next: boolean) { function setTerminalOpened(next: boolean) {
const current = store.terminal const current = store.terminal
@ -793,16 +798,26 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("terminal", "opened", next) setStore("terminal", "opened", next)
} }
function setReviewPanelOpened(next: boolean) { function setReviewPanelOpened(next: boolean, source: ReviewPanelSource) {
const nextSource = next ? source : "other"
const current = store.review const current = store.review
if (!current) { if (!current) {
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next }) batch(() => {
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next })
setEphemeral("reviewPanelSource", nextSource)
})
return return
} }
const value = current.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED const value = current.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED
if (value === next) return if (value === next) {
setStore("review", "panelOpened", next) if (ephemeral.reviewPanelSource !== nextSource) setEphemeral("reviewPanelSource", nextSource)
return
}
batch(() => {
setStore("review", "panelOpened", next)
setEphemeral("reviewPanelSource", nextSource)
})
} }
return { return {
@ -838,14 +853,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
reviewPanel: { reviewPanel: {
opened: reviewPanelOpened, opened: reviewPanelOpened,
open() { source: reviewPanelSource,
setReviewPanelOpened(true) open(source: ReviewPanelSource = "other") {
setReviewPanelOpened(true, source)
}, },
close() { close() {
setReviewPanelOpened(false) setReviewPanelOpened(false, "other")
}, },
toggle() { toggle() {
setReviewPanelOpened(!reviewPanelOpened()) setReviewPanelOpened(!reviewPanelOpened(), "other")
}, },
}, },
review: { review: {

View file

@ -2,11 +2,15 @@
transform: rotate(-90deg); transform: rotate(-90deg);
[data-slot="progress-circle-background"] { [data-slot="progress-circle-background"] {
stroke: var(--border-weak-base); stroke: var(--progress-circle-background, var(--border-weak-base));
}
[data-slot="progress-circle-background-overlay"] {
stroke: var(--progress-circle-background-overlay, transparent);
} }
[data-slot="progress-circle-progress"] { [data-slot="progress-circle-progress"] {
stroke: var(--border-active); stroke: var(--progress-circle-progress, var(--border-active));
transition: stroke-dashoffset 0.35s cubic-bezier(0.65, 0, 0.35, 1); transition: stroke-dashoffset 0.35s cubic-bezier(0.65, 0, 0.35, 1);
} }
} }

View file

@ -1,6 +1,6 @@
import { type ComponentProps, createMemo, splitProps } from "solid-js" import { type ComponentProps, createMemo, splitProps } from "solid-js"
export interface ProgressCircleProps extends Pick<ComponentProps<"svg">, "class" | "classList"> { export interface ProgressCircleProps extends Pick<ComponentProps<"svg">, "class" | "classList" | "style"> {
percentage: number percentage: number
size?: number size?: number
strokeWidth?: number strokeWidth?: number
@ -43,6 +43,13 @@ export function ProgressCircle(props: ProgressCircleProps) {
data-slot="progress-circle-background" data-slot="progress-circle-background"
stroke-width={strokeWidth()} stroke-width={strokeWidth()}
/> />
<circle
cx={center}
cy={center}
r={radius()}
data-slot="progress-circle-background-overlay"
stroke-width={strokeWidth()}
/>
<circle <circle
cx={center} cx={center}
cy={center} cy={center}