The dialog that closed itself before I could use it
Greenhouse had a gap that had been sitting quietly in the backlog for weeks: once you pointed the app at a folder during setup, that was it. There was no way to change your mind later, short of deleting the app’s settings and starting over. I finally got to it this week, and it turned out to be one of those tasks that looks like pure plumbing until it isn’t.
The reuse that seemed obvious
A few weeks earlier I’d built the onboarding screen that lets you point Greenhouse at an existing folder full of old projects, scan it, and decide what to bring in. That screen already had everything a “switch vaults” feature would need: a folder picker, a scanner, an assignment list. So the plan was simple - pull that logic out into its own reusable piece, and build a small dialog around it for switching.
The extraction itself went cleanly. The part that didn’t was a single word: what does “done” mean.
A callback with a hidden opinion
The original onboarding screen had a specific rule: if you’d already picked a folder and it turned out to have nothing left to sort through, unlock the “Next” button so you could move on. Sensible - you’re in a linear setup wizard, and there’s nothing to do here, so let the person continue.
When I pulled that logic into a shared piece, I renamed the signal from something wizard-specific to something generic - just “call this when there’s nothing left to decide.” Reasonable-sounding. I wired the new switch-vault dialog to close itself whenever that signal fired.
Then I actually opened the dialog. It closed itself instantly. Every time. Before I could click anything.
What I’d missed: the shared piece still checks its starting folder the moment it appears, exactly like the wizard step used to. In onboarding, seeing “nothing to do here” on your already-configured folder means “you’re done, move on.” In a dialog whose entire purpose is “maybe pick something different,” seeing “nothing to do here” on the folder you’re already in just means the dialog opened. Those are two completely different situations that happened to produce the exact same signal, and I’d assumed the signal’s meaning traveled with it when I moved the code. It didn’t. The code was identical; the sentence it was speaking to two different listeners meant two different things.
Fixing it without making the shared piece guess
The tempting fix is to make the shared component smarter - give it a flag like “should I auto-close when the starting folder is already resolved?” I didn’t want that. The moment a reusable piece starts asking “well, which situation am I in?”, you’ve pushed the judgment call inside the box where it’s harder to see and easier to get wrong the next time someone reuses it.
Instead, I left the shared piece exactly as blunt as it always was - it still announces “nothing left to decide” every single time that’s true, no exceptions, no context-sensitivity. The dialog itself now tracks one simple thing: has the person actually picked something new since it opened? Only if that’s true does “nothing left to decide” get treated as “we’re finished here.” Otherwise the announcement is just… noted, and ignored. The dumb, predictable core stayed dumb and predictable. The judgment call moved to the one place that actually has enough context to make it - the caller, not the tool.
What made me trust the fix
This is exactly the kind of bug a passing type-checker will never catch, because nothing about the types changed - a callback still fired, at the right time, with the right signature. The mismatch was entirely in what the firing was supposed to mean to whoever was listening. So the test I wrote wasn’t “does clicking things work” - it was specifically “open this dialog against a folder that’s already fully resolved, and confirm nothing closes.” That’s the one scenario where the old assumption would have silently reappeared if I’d gotten the fix even slightly wrong, and it’s exactly the scenario a quick manual click-through would never have surfaced, since a manual tester would have to think to try the “do nothing” path on purpose.
Related reading
Navigation redesign: topbar tabs and a Settings dialog
Eight footer controls at identical visual weight, an i18n key doing double duty, and the ARIA pattern the project's own a11y test refused to let me half-build.
Six small UX fixes in one sitting
Ultra-wide layout caps, locale-aware timestamps, emoji to SVG, dismissible errors, a first-run hero, and the word that showed up three times on one card.
Toast + undo for Greenhouse's vault action
The app's first transient-feedback surface, the aria-live rule that makes a conditionally-rendered toast silently unannounced, and a prop that goes stale the moment you rename.