fix(tui): anchor diff file menus to the screen (#47147)

This commit is contained in:
James Long
2026-09-04 16:23:16 -04:00
committed by GitHub
Unverified
parent 90cd910f52
commit 8ff1ef1a62
2 changed files with 88 additions and 70 deletions
@@ -0,0 +1,87 @@
/** @jsxImportSource @opentui/solid */
import type { Plugin } from "@opencode-ai/plugin/tui"
import { BoxRenderable, MouseButton } from "@opentui/core"
import { Portal, useTerminalDimensions } from "@opentui/solid"
import { createSignal, onCleanup } from "solid-js"
export function DiffFileMenu(props: {
context: Plugin.Context
state: { fileIndex: number; x: number; y: number }
reviewed: boolean
onToggle: () => void
onClose: () => void
}) {
const dimensions = useTerminalDimensions()
const theme = props.context.theme.contextual.overlay
const [hovered, setHovered] = createSignal(false)
const label = () => (props.reviewed ? "Mark incomplete" : "Mark complete")
const width = () => Math.min(19, dimensions().width)
const run = () => {
props.onClose()
props.onToggle()
}
onCleanup(props.context.keymap.mode.push("menu"))
props.context.keymap.layer(() => ({
mode: "menu",
commands: [
{ bind: "escape,ctrl+c", title: "Close file menu", group: "Diff", run: props.onClose },
{ bind: "return", title: label(), group: "Diff", run },
],
}))
return (
<Portal
ref={(container) => {
if (!(container instanceof BoxRenderable)) return
// Portal's wrapper must also escape root flow, not follow the full-height app.
container.position = "absolute"
container.left = 0
container.top = 0
container.zIndex = 2600
}}
>
<box
id="diff-file-menu-overlay"
position="absolute"
left={0}
top={0}
width={dimensions().width}
height={dimensions().height}
zIndex={2600}
onMouseDown={(event) => {
props.onClose()
event.preventDefault()
event.stopPropagation()
}}
>
<box
id="diff-file-menu"
position="absolute"
left={Math.max(0, Math.min(props.state.x, dimensions().width - width()))}
top={Math.max(0, Math.min(props.state.y + 1, dimensions().height - 1))}
width={width()}
height={1}
paddingLeft={1}
paddingRight={1}
backgroundColor={hovered() ? theme.background.action.primary.hovered : theme.background.default}
onMouseOver={() => setHovered(true)}
onMouseOut={() => setHovered(false)}
onMouseDown={(event) => {
if (event.button === MouseButton.RIGHT) props.onClose()
event.preventDefault()
event.stopPropagation()
}}
onMouseUp={(event) => {
event.preventDefault()
event.stopPropagation()
if (event.button === MouseButton.LEFT) run()
}}
>
<text fg={theme.text.default} selectable={false} wrapMode="none" truncate>
{label()}
</text>
</box>
</box>
</Portal>
)
}
@@ -14,6 +14,7 @@ import { filetype } from "../../util/filetype"
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js"
import { DiffViewerFileTree } from "./diff-viewer-file-tree"
import { DiffFileMenu } from "./diff-viewer-file-menu"
import { DiffViewerImage, isDiffImageFile } from "./diff-viewer-image"
import { DialogSelect } from "../../ui/dialog-select"
import { EmptyBorder } from "../../ui/border"
@@ -1076,76 +1077,6 @@ export function DiffViewerContent(props: {
)
}
function DiffFileMenu(props: {
context: Plugin.Context
state: FileMenuState
reviewed: boolean
onToggle: () => void
onClose: () => void
}) {
const dimensions = useTerminalDimensions()
const theme = props.context.theme.contextual.overlay
const [hovered, setHovered] = createSignal(false)
const label = () => (props.reviewed ? "Mark incomplete" : "Mark complete")
const run = () => {
props.onClose()
props.onToggle()
}
onCleanup(props.context.keymap.mode.push("menu"))
props.context.keymap.layer(() => ({
mode: "menu",
commands: [
{ bind: "escape,ctrl+c", title: "Close file menu", group: "Diff", run: props.onClose },
{ bind: "return", title: label(), group: "Diff", run },
],
}))
return (
<box
id="diff-file-menu-overlay"
position="absolute"
left={0}
top={0}
width={dimensions().width}
height={dimensions().height}
zIndex={2600}
onMouseDown={(event) => {
props.onClose()
event.preventDefault()
event.stopPropagation()
}}
>
<box
id="diff-file-menu"
position="absolute"
left={Math.max(0, Math.min(props.state.x, dimensions().width - 19))}
top={Math.max(0, Math.min(props.state.y + 1, dimensions().height - 1))}
width={19}
height={1}
paddingLeft={1}
paddingRight={1}
backgroundColor={hovered() ? theme.background.action.primary.hovered : theme.background.default}
onMouseOver={() => setHovered(true)}
onMouseOut={() => setHovered(false)}
onMouseDown={(event) => {
if (event.button === MouseButton.RIGHT) props.onClose()
event.preventDefault()
event.stopPropagation()
}}
onMouseUp={(event) => {
event.preventDefault()
event.stopPropagation()
if (event.button === MouseButton.LEFT) run()
}}
>
<text fg={theme.text.default} selectable={false}>
{label()}
</text>
</box>
</box>
)
}
function DiffViewerHelpDialog(props: { context: Plugin.Context; single: boolean }) {
const dimensions = useTerminalDimensions()
const theme = props.context.theme.contextual.elevated