Skip to content

Commit 84bddae

Browse files
authored
fix: flickering under very specific conditions (#406)
1 parent 52a9968 commit 84bddae

2 files changed

Lines changed: 51 additions & 102 deletions

File tree

src/ui/tui/primitives/ScreenContainer.tsx

Lines changed: 4 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,12 @@
66
* Each screen is wrapped in a ScreenErrorBoundary so that render crashes
77
* route to the outro screen with an error message instead of hanging.
88
*
9-
* Provides KeyboardHintsProvider context. The hints bar renders below
10-
* screen content but above any navigation chrome (e.g. tab bar).
11-
* Screens that have nav chrome (like TabContainer) push it into
12-
* navChromeRef so ScreenContainer can render it below the hints bar.
9+
* Provides KeyboardHintsProvider context. The hints bar is rendered below
10+
* screen content (inside the transition area) so all screens get it.
1311
*/
1412

1513
import { Box } from 'ink';
16-
import {
17-
createContext,
18-
useContext,
19-
useState,
20-
useCallback,
21-
useSyncExternalStore,
22-
type ReactNode,
23-
} from 'react';
14+
import { useSyncExternalStore, type ReactNode } from 'react';
2415
import { TitleBar } from '../components/TitleBar.js';
2516
import { useStdoutDimensions } from '../hooks/useStdoutDimensions.js';
2617
import { KeyboardHintsProvider } from '../hooks/useKeyboardHints.js';
@@ -38,43 +29,18 @@ function getContentWidth(terminalColumns: number): number {
3829
return Math.min(MAX_WIDTH, terminalColumns);
3930
}
4031

41-
// ── Nav chrome slot ──────────────────────────────────────────────────
42-
// Screens like TabContainer set nav chrome (tab bar, status bar) via
43-
// this context. ScreenContainer renders it below the hints bar.
44-
45-
interface NavChromeContextValue {
46-
setNavChrome(node: ReactNode): void;
47-
clearNavChrome(): void;
48-
}
49-
50-
export const NavChromeContext = createContext<NavChromeContextValue>({
51-
setNavChrome: () => undefined,
52-
clearNavChrome: () => undefined,
53-
});
54-
55-
export const useNavChrome = () => useContext(NavChromeContext);
56-
57-
// ── ScreenContainer ─────────────────────────────────────────────────
58-
5932
interface ScreenContainerProps {
6033
store: WizardStore;
6134
screens: Record<string, ReactNode>;
6235
}
6336

6437
export const ScreenContainer = ({ store, screens }: ScreenContainerProps) => {
6538
const [columns, rows] = useStdoutDimensions();
66-
const [navChrome, setNavChromeState] = useState<ReactNode>(null);
6739
useSyncExternalStore(
6840
(cb) => store.subscribe(cb),
6941
() => store.getSnapshot(),
7042
);
7143

72-
const setNavChrome = useCallback(
73-
(node: ReactNode) => setNavChromeState(node),
74-
[],
75-
);
76-
const clearNavChrome = useCallback(() => setNavChromeState(null), []);
77-
7844
const terminalWidth = columns;
7945
const width = getContentWidth(terminalWidth);
8046
const contentHeight = Math.max(5, rows - 3);
@@ -95,7 +61,6 @@ export const ScreenContainer = ({ store, screens }: ScreenContainerProps) => {
9561
>
9662
<ScreenErrorBoundary store={store}>
9763
<Box flexDirection="column" height={contentHeight}>
98-
{/* Screen content */}
9964
<Box
10065
flexDirection="column"
10166
flexGrow={1}
@@ -104,11 +69,8 @@ export const ScreenContainer = ({ store, screens }: ScreenContainerProps) => {
10469
>
10570
{activeScreen}
10671
</Box>
107-
{/* Hints bar — below content, above nav */}
10872
<Box height={1} />
10973
<KeyboardHintsBar />
110-
{/* Nav chrome pushed up by screens like TabContainer */}
111-
{navChrome}
11274
</Box>
11375
</ScreenErrorBoundary>
11476
</DissolveTransition>
@@ -124,11 +86,7 @@ export const ScreenContainer = ({ store, screens }: ScreenContainerProps) => {
12486
alignItems="center"
12587
justifyContent="flex-start"
12688
>
127-
<KeyboardHintsProvider>
128-
<NavChromeContext.Provider value={{ setNavChrome, clearNavChrome }}>
129-
{inner}
130-
</NavChromeContext.Provider>
131-
</KeyboardHintsProvider>
89+
<KeyboardHintsProvider>{inner}</KeyboardHintsProvider>
13290
</Box>
13391
);
13492
};

src/ui/tui/primitives/TabContainer.tsx

Lines changed: 47 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
/**
22
* TabContainer — Self-contained tabbed interface.
3+
* Absorbs BottomTabBar + StatusPanel functionality.
34
*
4-
* Tab content renders inline (ScreenContainer wraps it with the hints bar).
5-
* Navigation chrome (status bar + tab bar) is pushed to ScreenContainer
6-
* via NavChromeContext so it renders below the hints bar.
5+
* Key bindings are declared via useKeyBindings, which auto-registers
6+
* hints in the KeyboardHintsBar (rendered by ScreenContainer).
77
*/
88

99
import { Box, Text } from 'ink';
10-
import { useState, useMemo, useEffect, type ReactNode } from 'react';
10+
import { useState, useMemo, type ReactNode } from 'react';
1111
import { Colors, Icons } from '../styles.js';
1212
import {
1313
useKeyBindings,
1414
KeyMatch,
1515
type KeyBinding,
1616
} from '../hooks/useKeyBindings.js';
17-
import { useNavChrome } from './ScreenContainer.js';
1817
import type { WizardStore } from '../store.js';
1918

2019
export interface TabDefinition {
@@ -42,10 +41,8 @@ export const TabContainer = ({
4241
store,
4342
}: TabContainerProps) => {
4443
const [activeTab, setActiveTab] = useState(0);
44+
// Fallback to local state when no store is provided
4545
const [localExpanded, setLocalExpanded] = useState(false);
46-
const navChrome = useNavChrome();
47-
const setNavChrome = (node: ReactNode) => navChrome.setNavChrome(node);
48-
const clearNavChrome = () => navChrome.clearNavChrome();
4946

5047
const statusExpanded = store ? store.statusExpanded : localExpanded;
5148

@@ -96,57 +93,51 @@ export const TabContainer = ({
9693
expandableStatus && statusExpanded ? EXPANDED_COUNT : COLLAPSED_COUNT;
9794
const visibleMessages = allMessages.slice(-visibleCount);
9895

99-
// Push nav chrome to ScreenContainer
100-
useEffect(() => {
101-
setNavChrome(
102-
<Box flexDirection="column">
103-
{/* Status bar */}
104-
{visibleMessages.length > 0 && (
105-
<Box
106-
flexDirection="column"
107-
borderStyle="single"
108-
borderTop
109-
borderBottom={false}
110-
borderLeft={false}
111-
borderRight={false}
112-
borderColor={Colors.muted}
113-
paddingX={1}
114-
overflow="hidden"
115-
>
116-
{visibleMessages.map((msg, i, arr) => {
117-
const isCurrent = i === arr.length - 1;
118-
return (
119-
<Text key={i} color={Colors.muted} dimColor={!isCurrent}>
120-
{isCurrent ? Icons.diamond : '\u250A'} {msg}
121-
</Text>
122-
);
123-
})}
124-
</Box>
125-
)}
96+
return (
97+
<Box flexDirection="column" flexGrow={1}>
98+
{/* Active tab content — overflow hidden so expanded status eats into this area */}
99+
<Box flexDirection="column" flexGrow={1} flexShrink={1} overflow="hidden">
100+
{current?.component}
101+
</Box>
126102

127-
{/* Tab bar */}
128-
<Box height={1} />
129-
<Box gap={1} paddingX={1}>
130-
{tabs.map((tab, i) => (
131-
<Text
132-
key={tab.id}
133-
inverse={i === activeTab}
134-
color={i === activeTab ? Colors.accent : Colors.muted}
135-
bold={i === activeTab}
136-
>
137-
{` ${tab.label} `}
138-
</Text>
139-
))}
103+
{/* Status bar */}
104+
{visibleMessages.length > 0 && (
105+
<Box
106+
flexDirection="column"
107+
borderStyle="single"
108+
borderTop
109+
borderBottom={false}
110+
borderLeft={false}
111+
borderRight={false}
112+
borderColor={Colors.muted}
113+
paddingX={1}
114+
overflow="hidden"
115+
>
116+
{visibleMessages.map((msg, i, arr) => {
117+
const isCurrent = i === arr.length - 1;
118+
return (
119+
<Text key={i} color={Colors.muted} dimColor={!isCurrent}>
120+
{isCurrent ? Icons.diamond : '\u250A'} {msg}
121+
</Text>
122+
);
123+
})}
140124
</Box>
141-
</Box>,
142-
);
143-
return clearNavChrome;
144-
}, [activeTab, visibleMessages, tabs, setNavChrome, clearNavChrome]);
125+
)}
145126

146-
// Just render tab content — ScreenContainer handles the rest
147-
return (
148-
<Box flexDirection="column" flexGrow={1}>
149-
{current?.component}
127+
{/* Tab bar */}
128+
<Box height={1} />
129+
<Box gap={1} paddingX={1}>
130+
{tabs.map((tab, i) => (
131+
<Text
132+
key={tab.id}
133+
inverse={i === activeTab}
134+
color={i === activeTab ? Colors.accent : Colors.muted}
135+
bold={i === activeTab}
136+
>
137+
{` ${tab.label} `}
138+
</Text>
139+
))}
140+
</Box>
150141
</Box>
151142
);
152143
};

0 commit comments

Comments
 (0)