← the Atlas

Is Mobile Electricity?

analysis · evergreen ·

A Hickey/Lowy review of kolu's mobile support — is mobile an encapsulated change behind a receptacle, or smeared across every consumer measuring the voltage?

An adversarially-verified read of kolu’s mobile support through two lenses: Rich Hickey’s Simple Made Easy (is mobile complected through the app?) and Juval Lowy’s volatility-based decomposition (is mobile an encapsulated change, or smeared across every consumer?).

① The analogy — Lowy’s receptacle

Lowy uses household power to argue you decompose by what changes, not what it does:

“Power in a house is highly volatile: AC or DC; 110 or 220 volts; 50 or 60 hertz; solar, generator, or grid. All that volatility is encapsulated behind a receptacle. When it is time to consume power, all the user sees is an opaque receptacle.” — Juval Lowy, Righting Software

The toaster never exposes the wires or measures the frequency — it plugs in. Every appliance carrying its own voltmeter and deciding what to do with the raw mains is what Lowy calls functional decomposition and Hickey calls complecting. So the question is precise: does feature code plug into a stable interface that already resolved “where am I running,” or does each consumer measure the voltage (isMobile()) and branch itself? The answer for kolu: two outlets, one good extension cord, and a lot of bare wire.

② The circuit as wired today

One source, two voltages, two clean outlets — then exposed mains:

kolu mobile wiring — one source, two receptacles, and the bare-mains consumersuseMobile.tsisMobile · 639pxisTouch · coarse⚡ THE SOURCE (two circuits)OUTLET ✓ App.tsx:546match(isMobile())→ MobileTileView | TerminalCanvasleaves plug in blind · no per-leaf ifOUTLET ✓ withKeyboardDismissisTouch guard lives inside4 drawers inject only their setter⚠ BARE MAINSraw isMobile() / isTouch() exportedno posture / capability interface~18 consumers each measure the voltage:🔌 useTips.ts:65,73 — suppress tips ×2🔌 commands.tsx:318 — erase canvas section🔌 openInCodeTab.ts:76 — fork the intent🔌 CodeTab.tsx:103/548 — density + scroll🔌 App.tsx:194/225 — center / switcher🔌 Terminal.tsx:538 — contenteditable surgery⚡ VOLTAGE MISMATCHJS receptacle rated 639pxCSS receptacle (Tailwind sm:) 640px639–640 band: JS says mobile,CSS says desktop. Comment misclaims “match”.
Encapsulated — appliance plugs in blindBare mains — consumer measures the voltageTwo voltages for one threshold

③ Scorecard

DimensionGradeNote
Single source of truthCEach concept has one signal — but the breakpoint value was defined twice (JS 639px vs Tailwind sm: 640px); the comment misclaimed they match. fixed · #1088 — the JS query now derives from the --breakpoint-sm token.
EncapsulationCTwo receptacles done right; ~18 consumers branch on the raw signal with no posture/capability layer.
Concept separationBisMobile (size) and isTouch (modality) are orthogonal; useViewPosture refuses to fold mobile in.
CSS vs JS disciplineD18 JS signal-reads vs 4 Tailwind classes, 0 custom @media. CSS owns almost no structure.
Component duplicationBVerification refuted the alarms: row/pip/metadata logic is already shared. Only reviewer-approved JSX shells diverge.
Consumer leakageDopenInCodeTab forks intent inline; tips/commands/canvas each ask “am I mobile?”; the feature layer is functionally decomposed.

④ What’s wired right — the parts that already are electricity

⑤ Where the leads are bare — five leaks that survived verification

⑥ Where the analogy honestly breaks — a phone is not a 110-volt desktop

Some mobile volatility is not a different voltage of the same signal; it is a categorically different appliance. A soft keyboard is not a 220V keyboard — it’s a different input device with its own focus model and contenteditable target. The pan/zoom canvas is genuinely unusable on a phone, so MobileTileView and the drawers are a second, correctly-built product surface. This is why “just move it all to CSS” is wrong:

There are four concepts wearing one word — viewport-size, touch-modality, layout-posture, feature-availability. The first two are correctly receptacled; the second two are functionally decomposed. The fix is to add the missing posture and capability receptacles, not to erase the divergence.

⑦ The receptacle that should exist — keep the wiring, add three faceplates

Keep the wiring. isMobile and isTouch stay as the two axes — but unify the rating: register the breakpoint once (Tailwind v4 @theme { --breakpoint-sm } or a shared constant) and derive the JS createMediaQuery from the same number. The 639/640 desync and the misleading comment both vanish. Then three named seams:

  1. useRightPanel.reveal() — one verb that internally resolves drawer-open (mobile) vs uncollapse (desktop). openInCodeTab and every future producer call it and never read isMobile. ~5 lines; highest payoff-to-effort fix.
  2. A resolved capability objectlayout.supportsSpatialCanvas, showsAmbientTips, isCompact — computed once, each from the right axis. Consumers ask about capability, not pixels.
  3. enableSoftKeyboardInput(term) — owns the contenteditable knowledge + xterm poking so Terminal.tsx calls one verb; re-key CodeTab’s touch-scroll off touch capability rather than viewport.

withKeyboardDismiss is the existence proof these work: a drawer plugs in by passing its setter and never measures the voltage.

⑧ What was done — ranked by payoff ÷ effort

ActionWhyEffortPayoffStatus
Unify the breakpoint (639px ⟶ one value)Register --breakpoint-sm once + derive the JS query; fix the contradicting comment. Kills the desync band + orphaned ChromeBar sm:.smallhighshipped · #1088
Add useRightPanel.reveal()Move the isMobile fork out of openInCodeTab into one host-owned verb; mirrors withKeyboardDismiss. Near-zero blast radius.smallhighshipped · #1088
A resolved capability seamReplace scattered isMobile() feature checks with one resolved object, each keyed off the right axis.mediummediumshipped · #1088
enableSoftKeyboardInput(term)Wrap the contenteditable surgery behind a named seam; re-key CodeTab touch-scroll off touch capability.mediummediumshipped · #1088
Do not extract the dock-row / metadata JSX shellsThe volatile logic is already shared; only JSX shells diverge, for documented touch-target reasons two reviewers chose. A forced BaseRow would be worse.smallguardstanding

The one-line answer at review time: mobile is electricity for the macro layout switch and the keyboard-dismiss wrapper, and bare mains everywhere else. The wiring discipline was good — two correctly-separated circuits — but neither terminated in enough receptacles, and the one threshold was rated at two voltages. #1088 added the missing receptacles and unified the rating.


Method · 24 subagents over a 4-phase workflow: 5 parallel mappers (66 findings) → independent Hickey + Lowy lenses → 16 structural claims adversarially re-read against source → reconciled synthesis. Source: kolu/packages/client/src, branch brave-second, 2026-06-01.