fix(tui): breathing room above the composer cluster, status tight to input

Previous revision added marginTop={1} to the input which stacked as a
phantom gap BETWEEN status and input. The breathing row should sit
ABOVE the status-in-top cluster, not inside it.

- StatusRulePane at="top" now carries its own marginTop={1} so it
  always has a one-row gap above (separating it from transcript or,
  when queue is present, from the last queue item)
- Input Box marginTop flips: 0 in top mode (status is the separator),
  1 in bottom/off mode (input itself caps the composer cluster)
- Net: status and input are tight together in 'top'; input and status
  are tight together at the bottom in 'bottom'; one-row breathing room
  above whichever element sits on top of the cluster
This commit is contained in:
Brooklyn Nicholson 2026-04-22 14:28:47 -05:00
parent 408fc893e9
commit a7cc903bf5
2 changed files with 34 additions and 28 deletions

View file

@ -1,6 +1,6 @@
import { Box, type ScrollBoxHandle, Text } from '@hermes/ink'
import { Box, Text, type ScrollBoxHandle } from '@hermes/ink'
import { useStore } from '@nanostores/react'
import { type ReactNode, type RefObject, useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore, type ReactNode, type RefObject } from 'react'
import { $delegationState } from '../app/delegationStore.js'
import { $turnState } from '../app/turnStore.js'

View file

@ -184,7 +184,16 @@ const ComposerPane = memo(function ComposerPane({
<StatusRulePane at="top" composer={composer} status={status} />
{!isBlocked && (
<>
<Box flexDirection="column" marginTop={ui.statusBar === 'top' ? 0 : 1} position="relative">
<FloatingOverlays
cols={composer.cols}
compIdx={composer.compIdx}
completions={composer.completions}
onModelSelect={actions.onModelSelect}
onPickerSelect={actions.resumeById}
pagerPageSize={composer.pagerPageSize}
/>
{composer.inputBuf.map((line, i) => (
<Box key={i}>
<Box width={3}>
@ -196,15 +205,6 @@ const ComposerPane = memo(function ComposerPane({
))}
<Box position="relative">
<FloatingOverlays
cols={composer.cols}
compIdx={composer.compIdx}
completions={composer.completions}
onModelSelect={actions.onModelSelect}
onPickerSelect={actions.resumeById}
pagerPageSize={composer.pagerPageSize}
/>
<Box width={pw}>
{sh ? (
<Text color={ui.theme.color.shellDollar}>$ </Text>
@ -230,7 +230,7 @@ const ComposerPane = memo(function ComposerPane({
</Box>
</Box>
</Box>
</>
</Box>
)}
{!composer.empty && !ui.sid && <Text color={ui.theme.color.dim}> {ui.status}</Text>}
@ -264,22 +264,28 @@ const StatusRulePane = memo(function StatusRulePane({
return null
}
// 'top' sits inline above the input; give it one row of breathing
// space above so the transcript (or queue) doesn't butt directly
// against the status row. 'bottom' lives at the last row of the
// viewport so it needs no margin.
return (
<StatusRule
bgCount={ui.bgTasks.size}
busy={ui.busy}
cols={composer.cols}
cwdLabel={status.cwdLabel}
model={ui.info?.model?.split('/').pop() ?? ''}
sessionStartedAt={status.sessionStartedAt}
showCost={ui.showCost}
status={ui.status}
statusColor={status.statusColor}
t={ui.theme}
turnStartedAt={status.turnStartedAt}
usage={ui.usage}
voiceLabel={status.voiceLabel}
/>
<Box flexShrink={0} marginTop={at === 'top' ? 1 : 0}>
<StatusRule
bgCount={ui.bgTasks.size}
busy={ui.busy}
cols={composer.cols}
cwdLabel={status.cwdLabel}
model={ui.info?.model?.split('/').pop() ?? ''}
sessionStartedAt={status.sessionStartedAt}
showCost={ui.showCost}
status={ui.status}
statusColor={status.statusColor}
t={ui.theme}
turnStartedAt={status.turnStartedAt}
usage={ui.usage}
voiceLabel={status.voiceLabel}
/>
</Box>
)
})