mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
Let terminal pane controls float over output
This commit is contained in:
@@ -166,14 +166,14 @@
|
||||
--terminal-pane-title-on-dark-fg: rgb(255 255 255 / 0.52);
|
||||
--terminal-pane-title-on-dark-input-fg: rgb(255 255 255 / 0.7);
|
||||
--terminal-pane-title-on-dark-placeholder: rgb(255 255 255 / 0.38);
|
||||
--terminal-pane-title-on-dark-button-fg: rgb(255 255 255 / 0.3);
|
||||
--terminal-pane-title-on-dark-button-fg: rgb(255 255 255 / 0.48);
|
||||
--terminal-pane-title-on-dark-button-hover-fg: rgb(255 255 255 / 0.8);
|
||||
--terminal-pane-title-on-dark-input-bg: rgb(255 255 255 / 0.04);
|
||||
--terminal-pane-title-on-dark-separator: rgb(255 255 255 / 0.06);
|
||||
--terminal-pane-title-on-light-fg: rgb(24 24 27 / 0.64);
|
||||
--terminal-pane-title-on-light-input-fg: rgb(24 24 27 / 0.82);
|
||||
--terminal-pane-title-on-light-placeholder: rgb(24 24 27 / 0.48);
|
||||
--terminal-pane-title-on-light-button-fg: rgb(24 24 27 / 0.42);
|
||||
--terminal-pane-title-on-light-button-fg: rgb(24 24 27 / 0.58);
|
||||
--terminal-pane-title-on-light-button-hover-fg: rgb(24 24 27 / 0.82);
|
||||
--terminal-pane-title-on-light-input-bg: rgb(24 24 27 / 0.05);
|
||||
--terminal-pane-title-on-light-separator: rgb(24 24 27 / 0.1);
|
||||
@@ -247,14 +247,14 @@
|
||||
--terminal-pane-title-on-dark-fg: rgb(255 255 255 / 0.52);
|
||||
--terminal-pane-title-on-dark-input-fg: rgb(255 255 255 / 0.7);
|
||||
--terminal-pane-title-on-dark-placeholder: rgb(255 255 255 / 0.38);
|
||||
--terminal-pane-title-on-dark-button-fg: rgb(255 255 255 / 0.3);
|
||||
--terminal-pane-title-on-dark-button-fg: rgb(255 255 255 / 0.48);
|
||||
--terminal-pane-title-on-dark-button-hover-fg: rgb(255 255 255 / 0.8);
|
||||
--terminal-pane-title-on-dark-input-bg: rgb(255 255 255 / 0.04);
|
||||
--terminal-pane-title-on-dark-separator: rgb(255 255 255 / 0.06);
|
||||
--terminal-pane-title-on-light-fg: rgb(24 24 27 / 0.64);
|
||||
--terminal-pane-title-on-light-input-fg: rgb(24 24 27 / 0.82);
|
||||
--terminal-pane-title-on-light-placeholder: rgb(24 24 27 / 0.48);
|
||||
--terminal-pane-title-on-light-button-fg: rgb(24 24 27 / 0.42);
|
||||
--terminal-pane-title-on-light-button-fg: rgb(24 24 27 / 0.58);
|
||||
--terminal-pane-title-on-light-button-hover-fg: rgb(24 24 27 / 0.82);
|
||||
--terminal-pane-title-on-light-input-bg: rgb(24 24 27 / 0.05);
|
||||
--terminal-pane-title-on-light-separator: rgb(24 24 27 / 0.1);
|
||||
|
||||
@@ -353,6 +353,11 @@
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
padding: 0 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pane-title-bar[data-chromeless] .pane-title-actions {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.pane-title-close {
|
||||
|
||||
@@ -1760,27 +1760,24 @@ export default function TerminalPane({
|
||||
return subscribeTerminalPaneAttention(tabId, applyTerminalPaneAttention)
|
||||
}, [tabId, paneCount, applyTerminalPaneAttention])
|
||||
|
||||
// Sync the pane title reservation before paint. The title UI stays outside
|
||||
// xterm's DOM, but xterm must still fit below it so the first terminal row is
|
||||
// never hidden under the title strip.
|
||||
// Sync the pane title reservation before paint. Text/banner chrome stays
|
||||
// outside xterm's DOM, but xterm must still fit below it so the first
|
||||
// terminal row is never hidden under meaningful status UI.
|
||||
useLayoutEffect(() => {
|
||||
const manager = managerRef.current
|
||||
if (!manager) {
|
||||
return
|
||||
}
|
||||
// Show the title bar space when the pane has a title, is being
|
||||
// inline-edited, or has transient startup chrome. Unread activity does
|
||||
// not reserve this space; its overlay should not reflow terminal content.
|
||||
// Show title space only for text/status chrome. Chromeless pane controls
|
||||
// float over xterm so untitled panes keep their first row.
|
||||
const needsFit = syncSessionRestoredBannerTitleSpace({
|
||||
panes: manager.getPanes(),
|
||||
paneTitles,
|
||||
renamingPaneId,
|
||||
sessionRestoredBannerPaneIds,
|
||||
reservePaneHeaderSpace: isActive && (isVisible || shouldMeasureHiddenStartup)
|
||||
sessionRestoredBannerPaneIds
|
||||
})
|
||||
if (needsFit && (isVisible || shouldMeasureHiddenStartup)) {
|
||||
// Why: inactive terminal tabs can drop active-only header reservation.
|
||||
// Fitting that hidden geometry changes PTY rows and wakes TUIs with
|
||||
// Why: fitting hidden geometry changes PTY rows and wakes TUIs with
|
||||
// SIGWINCH; the visible resume path owns any real layout correction.
|
||||
fitPanes(manager)
|
||||
}
|
||||
@@ -1790,7 +1787,6 @@ export default function TerminalPane({
|
||||
paneTitles,
|
||||
renamingPaneId,
|
||||
sessionRestoredBannerPaneIds,
|
||||
isActive,
|
||||
isVisible,
|
||||
shouldMeasureHiddenStartup
|
||||
])
|
||||
|
||||
@@ -127,11 +127,13 @@ describe('TerminalPaneHeaderOverlay', () => {
|
||||
expect(onClosePane).not.toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
it('keeps close-pane available for untitled split pane headers', () => {
|
||||
it('keeps split and close-pane controls available for untitled split pane headers', () => {
|
||||
const { container, onClosePane, onRemoveTitle } = renderOverlay({
|
||||
paneTitles: { 1: '', 2: '' }
|
||||
})
|
||||
|
||||
expect(container.querySelector('button[aria-label="Split Terminal Right"]')).not.toBeNull()
|
||||
expect(container.querySelector('.pane-title-drag-handle')).toBeNull()
|
||||
const closePane = container.querySelector<HTMLButtonElement>('button[aria-label="Close Pane"]')
|
||||
expect(closePane).not.toBeNull()
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ export default function TerminalPaneHeaderOverlay({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{paneCount > 1 && (
|
||||
{paneCount > 1 && !isChromeless && (
|
||||
<div
|
||||
className="pane-title-drag-handle"
|
||||
aria-hidden="true"
|
||||
@@ -205,7 +205,7 @@ export default function TerminalPaneHeaderOverlay({
|
||||
{title}
|
||||
</button>
|
||||
) : null}
|
||||
<div className="pane-title-actions ml-auto flex shrink-0 items-center gap-0.5">
|
||||
<div className="pane-title-actions ml-auto flex shrink-0 items-center gap-0">
|
||||
{showAlwaysOnHeaders ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
+20
-5
@@ -76,7 +76,7 @@ describe('session restored banner pane state', () => {
|
||||
expect(paneText(createdPane)).toBe(SESSION_RESTORED_BANNER_TEXT)
|
||||
})
|
||||
|
||||
it('reserves title space for always-on pane headers on the active tab', () => {
|
||||
it('does not reserve title space for chromeless always-on pane headers', () => {
|
||||
const activePane = createPane(1)
|
||||
const secondPane = createPane(2)
|
||||
|
||||
@@ -84,13 +84,28 @@ describe('session restored banner pane state', () => {
|
||||
panes: [activePane, secondPane],
|
||||
paneTitles: {},
|
||||
renamingPaneId: null,
|
||||
sessionRestoredBannerPaneIds: new Set(),
|
||||
reservePaneHeaderSpace: true
|
||||
sessionRestoredBannerPaneIds: new Set()
|
||||
})
|
||||
|
||||
expect(needsFit).toBe(false)
|
||||
expect(activePane.container.hasAttribute('data-has-title')).toBe(false)
|
||||
expect(secondPane.container.hasAttribute('data-has-title')).toBe(false)
|
||||
})
|
||||
|
||||
it('reserves title space for explicit titles and inline rename', () => {
|
||||
const titledPane = createPane(1)
|
||||
const renamingPane = createPane(2)
|
||||
|
||||
const needsFit = syncSessionRestoredBannerTitleSpace({
|
||||
panes: [titledPane, renamingPane],
|
||||
paneTitles: { [titledPane.id]: 'server' },
|
||||
renamingPaneId: renamingPane.id,
|
||||
sessionRestoredBannerPaneIds: new Set()
|
||||
})
|
||||
|
||||
expect(needsFit).toBe(true)
|
||||
expect(activePane.container.hasAttribute('data-has-title')).toBe(true)
|
||||
expect(secondPane.container.hasAttribute('data-has-title')).toBe(true)
|
||||
expect(titledPane.container.hasAttribute('data-has-title')).toBe(true)
|
||||
expect(renamingPane.container.hasAttribute('data-has-title')).toBe(true)
|
||||
})
|
||||
|
||||
it('renders and reserves title space only on the restored inactive split pane', async () => {
|
||||
|
||||
@@ -88,12 +88,10 @@ export function syncSessionRestoredBannerTitleSpace(args: {
|
||||
paneTitles: Readonly<Record<number, string>>
|
||||
renamingPaneId: number | null
|
||||
sessionRestoredBannerPaneIds: ReadonlySet<number>
|
||||
reservePaneHeaderSpace?: boolean
|
||||
}): boolean {
|
||||
let needsFit = false
|
||||
for (const pane of args.panes) {
|
||||
const shouldShow =
|
||||
args.reservePaneHeaderSpace === true ||
|
||||
!!args.paneTitles[pane.id] ||
|
||||
args.renamingPaneId === pane.id ||
|
||||
args.sessionRestoredBannerPaneIds.has(pane.id)
|
||||
|
||||
Reference in New Issue
Block a user