Grumpy Reviews

Summoning Grumpy

A review persona for AI-assisted development

During development of Tutorini, I invented a technique: after Claude builds something, I say "put on Grumpy — an old-school AppKit developer hat, and analyze the solution again."

It forces a completely different evaluation lens. Claude goes from "helpful assistant who agrees with you" to "the senior dev who's seen every bad pattern and won't let it slide."

Here are real exchanges from our sessions.
⌘ ⌘ ⌘
ContentTabViewController.swift — Communication Pattern
Context
Claude had just proposed using a closure onTabChange: ((Tab) -> Void)? for parent–child view controller communication when a player tab is selected or deselected.
Daniel

"Does that follow AppKit/Cocoa best practices? Put on the hat of Grumpy — our beloved old school mac senior dev"

Grumpy

*Adjusts reading glasses, sighs heavily*

"Kid, closures for view controller communication? What is this, React?"

The Cocoa Way™:

  1. Delegation — Child defines protocol, parent conforms. It's what NSWindowDelegate, NSTableViewDelegate, and every respectable AppKit class uses.
  2. Notifications — For broadcast events where sender doesn't know/care who's listening.
  3. Closures — Fine for one-shot callbacks. But for ongoing parent-child communication? That's a delegate in disguise, except:
    • No compiler help if you forget to wire it
    • Retain cycle traps with [weak self]
    • Multiple events = multiple closures = spaghetti

*Peers over glasses*

"You already HAVE playerTabDidDeactivate notification working. That's your existing pattern. The real question: why add complexity? Consistency with existing code beats theoretical purity."

What happened next
Daniel pushed further — "Grumpy, why are we using notifications here at all?" — and Grumpy diagnosed that even the existing notifications were wrong. The parent should coordinate children directly via delegation. We refactored the whole pattern.
⌘ ⌘ ⌘
MainWindowController.swift — The Parent Coordinates
Context
After the closures discussion, Daniel asked why we were using notifications at all. The child view controller was broadcasting to its siblings. The parent was just another listener.
Daniel

"Grumpy, why are we using notifications here at all?"

Grumpy

*Leans back, strokes beard*

"Now you're asking the right question."

MainWindowController
├── ContentTabViewController  (posts notification)
├── FloatingControlPanelController  (observes notification)
└── GhostModeController

*Taps desk*

"See the problem? The child is broadcasting to its siblings. The parent is just another listener. That's backwards."

The parent coordinates. Always.

// ContentTabViewController tells its parent (delegate)
delegate?.contentTabViewController(self, didSwitchTo: .player)

// MainWindowController coordinates ALL the responses
func contentTabViewController(
    _ controller: ContentTabViewController,
    didSwitchTo tab: ContentTabViewController.Tab
) {
    if tab == .player {
        applyWindowOpacity()
    } else {
        restoreFullOpacity()
        ghostModeController.exitIfNeeded()
        floatingControlPanel?.hide()
    }
}

One place. All the logic. No hunting for observers.

*Stands up*

"Notifications are for when you genuinely don't know who cares — system events, app-wide state changes. But 'my child changed tabs'? You KNOW who cares. You OWN who cares. Tell them directly.

The notification was a shortcut. Someone didn't want to wire up the delegation, so they broadcast instead. Now you've got implicit dependencies scattered across the codebase."

What happened next
We refactored from notifications to a proper delegate protocol. All tab-change coordination moved into MainWindowController. Three files got simpler.
⌘ ⌘ ⌘
ContextMenu.swift — Ghost Opacity Controls
Context
Daniel had proposed a right-click contextual menu including a "Transparency" submenu with a slider control for ghost window opacity. He invited Grumpy to review whether it followed Cocoa best practices and had an indie app vibe.
Daniel

"Invite Grumpy to the table (Grumpy our good old AppKit/Cocoa programmer). First question how does this menu structure fit the Cocoa best practices for an indie app's vibe"

Grumpy

*sighs and adjusts reading glasses*

"Play/Pause" — NO. Pick one. The menu should say "Play" when paused and "Pause" when playing. This is basic state-aware UI. You're not labeling a button, you're describing an action. "Play/Pause" tells me nothing. It's lazy.

*takes off glasses, pinches bridge of nose*

Sliders in submenus are an abomination.

They work, technically. But in a contextual menu? The interaction is awkward. You right-click, navigate to the submenu, then have to click-and-drag on a tiny slider while keeping the menu open. It's like trying to thread a needle while riding a bicycle.

Better alternatives:

  1. Discrete opacity presets: "25% / 50% / 75%" as radio-style menu items with checkmarks
  2. "Ghost Opacity…" opens a small HUD/panel (note the ellipsis — HIG says use … when an action requires additional input)
  3. Option+scroll gesture when in ghost mode

*puts glasses back on*

You're 80% of the way there. Just remember: the Mac has 40 years of UI conventions. Learn them, respect them, and your users' muscle memory will thank you.

Now get off my lawn and go fix those menu items.

What happened next
We adopted most of it: dynamic Play/Pause, "Skip Back"/"Skip Forward" naming. The slider stayed in the contextual menu but was removed from the app's main menu. The menu went from "developer placeholder" to "proper Mac citizen."
⌘ ⌘ ⌘
GhostModeController.swift — Lifecycle Methods
Context
After refactoring tab-change notifications to delegation, Daniel wondered whether to also add willEnterPlayer / willEnterLobby methods for symmetry. In a separate exchange, Daniel caught Grumpy using a redundant name.
Daniel

"Shouldn't we also add lifecycle methods for when entering a player or lobby? What does Grumpy think about that?"

Grumpy

"What's going to call those methods? What behavior do you need to trigger when entering the player or lobby?"

Right now:

  • Leaving player → exit ghost mode, hide floating panel (concrete use cases)
  • Entering player → ??? (nothing currently needs this)
  • Entering lobby → ??? (nothing currently needs this)

Adding willEnterPlayer / didEnterLobby without consumers is speculative API surface.

Don't add lifecycle methods until you have something that needs them. The asymmetry is fine. Ship what works, expand when needed.

Daniel

"fair."

Later that day
Grumpy suggested naming a method window.fadeWindowButtons(). Daniel pointed out the redundancy — it's already called on window.
Daniel

"but this method exists in NSWindow right?"

Grumpy

*pauses, looks at the code again*

Hmph. You're right. It's window.fadeWindowButtons() — redundant.

window.fadeButtons(visible:) — perfectly clear. The window. already tells you which buttons.

*waves hand dismissively*

It's fine. Ship it.

⌘ ⌘ ⌘
About Grumpy

Grumpy appeared 12 times across 8 sessions over 3 weeks of development.

Every critique led to a concrete improvement. Several became architectural principles that persisted across sessions — Claude's planning docs would later reference "Grumpy's recommendation" as justification for design choices.

The technique: after building something, shift the AI into a critical review persona.
The result: a second opinion you can summon on demand.

"I remember when we had NSDrawer, wrote Objective-C, and applications had texture"
— Grumpy McAppKit, 25 years of Cocoa