Notes

Thoughts on design and product

The Cost of Making

I built a small app recently that blocks distracting websites and nudges me to read. When I navigate to Reddit, it opens an epub instead. It took a morning while I was having coffee. It does exactly what I need and nothing else.

ReadIt screenshot
ReadIt screenshot

It’s not an isolated example. I’ve made a handful of these little apps just for me, each one scratching an itch and saving a few dollars. An iOS app that keeps the screen awake while displaying a recipe (or anything else). A prototyping boilerplate. A night-vision ereader that doesn’t glow in bed. A little notepad that lives in the menu bar.

A shift in thinking

Something changed in how I think about software. It used to be that when I needed a tool, I searched for an app to handle it. Compare options, justify the subscription. The usual routine. Now the first question is whether I can just build it myself.

The answer is increasingly yes, and quickly. The cost of building collapsed. Financial cost, sure. But also time. Things that would have taken weeks take an afternoon. Things I’d abandon halfway through, I finish. Things that would’ve sat in a half-finished state get polished enough to use routinely.

Robin Sloan wrote about apps as home-cooked meals. Software made for the people you know, or just for yourself. No users, no roadmap, no scale. I keep coming back to that idea because it captures something I’m experiencing but hadn’t named.

Most software, notably SaaS, is built to serve many people. The SaaS model depends on a specific assumption: that building & maintaining is expensive enough that renting makes sense. For decades, that held. It’s starting not to. When you can build something in an afternoon that replaces a $10/month subscription, the math changes. Not for everyone, not for every tool. But at the margins, for the kinds of small, specific utilities that make up daily work, the economics are shifting.

Another thing. To make software profitable, you need scale efficiencies. One product needs to support many people. The consequence is that they’re full of compromises. Features you don’t need, workflows that don’t match yours, settings panels you’ll never touch. Joel Spolsky, over 20 years ago, talked about the 80/20 myth. You can try to build 80% of the most valuable features, but each person will have a different perspective on what the 80% means. Consequently, software tends to accrete bloat.

What I hope to see

What does this mean for commercial software when building is so easy? I don’t believe it will evaporate, but I have a feeling it will change dramatically. I could envision people creating highly personalized software, but built upon composable primitives that afford security, reliability, and interoperability. I imagine seeing more niche software designed for micromarkets. I could see greater attention given to single-purpose features, something at the level of Skills or workflows.

Maybe software evolves to look less like a monolithic grand piano, and more like a closet full of synths that can be chained together on the fly.

It’s going to be weird and I’m here for it.

How Chat UIs Render Widgets

I was building a chat interface that could render rich UI widgets. For my project, I wanted to be able to render weekly plans for a training app. But a question occurred to me. “How does the LLM know when and how to render each widget?”

Plan my week

Proposed weekly plan


MonClimbingWedStrengthFriClimbingSatConditioning

The misconception

My first instinct was that I’d need to teach the model about each widget. Describe the component structure, its expected inputs, maybe even its visual layout. This felt immediately wrong. Coupling an LLM to a specific UI framework seemed fragile and needlessly complex.

What actually happens

The answer is simpler than I expected. The LLM doesn’t need to know anything about the widget. It just needs to return structured JSON that conforms to a schema you define. Something like:

{ "type": "plan", "data": [{ "day": "Mon", "workout": "Climbing" }...]}

A separate piece of your application reads that response, looks at the type field, and mounts the corresponding component with the right props. The LLM and the UI never touch each other directly. The schema is the contract between them.

LLM to UI Widget rendering flow

A horizontal flowchart showing three stages: an LLM box on the left outputs JSON to a hatched Application box in the center, which then mounts the correct UI Widget shown on the right. Arrows connect each stage left to right.

LLMJSONApplicationrenderUI Widget

Once I saw it this way, the pattern felt obvious. It’s the same separation of concerns that shows up everywhere in software: data and presentation, decoupled by a contract.

Creating a Skill for Illustrations

I’ve wanted more illustrations on this site for a while. Not stock graphics or generic icons, but diagrams with a specific feel: minimal, monochrome, the kind of vintage technical drawings you’d find in a scientific paper or an old tool catalog. Hatching patterns instead of gradients. No color, no fluff. Something that could sit quietly alongside text without demanding attention.

Drawing each one by hand would work, but it’s slow and inconsistent. I’d inevitably drift from the original vision. So I decided to teach Claude how to make them instead.

Step 1: Draw reference examples

I started in Figma, sketching a few illustrations that demonstrated the stylistic conventions I wanted. These weren’t specific diagrams for specific posts. They were pure style references: shapes with hatching fills, simple flowcharts, layered rectangles.

A few key qualities I focused on: minimal line work, monochrome palette that adapts to dark mode, tight compositions without excess whitespace, and fill patterns that feel like they could be printed on a transparency.

Step 2: Export and prompt

I exported the Figma drawings as SVGs and fed them to Claude as concrete references. Then I prompted it to help me create a skill document, a structured file that captures how to reproduce this style consistently.

The examples did most of the heavy lifting. Instead of trying to describe “vintage technical diagram aesthetic” in words, I could point to the actual thing.

Step 3: Document the skill

The skill file covers three areas: aesthetic principles (what it should feel like), functional requirements (how it should behave), and usage context (where it lives).

For aesthetics, I documented the vintage scientific inspiration, the preference for hatching and stippling over gradients, and the importance of restraint. For function, I specified that all colors use currentColor so illustrations automatically adapt to light and dark modes. ViewBoxes should be cropped tight. Accessibility attributes are required. For context, I noted that these appear inline in blog posts and case studies, sized for the content column.

Here’s what the output looks like:

Sequence Diagram

A sequence diagram showing three vertical lifelines with horizontal arrows indicating message flow between them in a top-to-bottom sequence.

Returning to Cursor

I’ve been using Claude Code in the terminal for a while now, and it’s been genuinely useful. I built out a pretty nice command center for building: zellij for terminal multiplexing, ghostty for the terminal emulator, and lazygit for version control UI. It was just enough and it worked well for me.

But I’m checking out Cursor again, and it’s not just about personal preference. Many of the designers at Zapier are starting to use Cursor over tools like V0, and if I’m going to be helpful when they run into issues or have questions about how to use it effectively, I need to understand their experience. It’s hard to offer good advice about a tool you’re not actively using.

Beyond that practical reason, I’m finding myself reaching for Cursor more often anyway. Not because it’s radically different, but because it removes just enough friction that I’m actually enjoying the experience.

Better formatting, less cognitive load

The first thing I noticed was how much more readable everything is. Markdown renders properly, code blocks are syntax-highlighted, and the overall text formatting just feels more polished. It’s a small thing, but when you’re spending hours reading AI-generated responses, readability matters.

It doesn’t fight my workflow

Here’s what turned me off from other AI coding tools in the past: they tried to “help” by changing how I work. Forcing me into work trees, creating unusual directory structures, adding layers of abstraction I didn’t ask for. It felt like I was learning their system rather than augmenting mine.

Cursor doesn’t do that. It integrates with my existing workflow rather than replacing it. I can use normal git commands, create branches, push and pull, close out branches—all the familiar patterns I’ve built up over years.

And crucially, it’s accessible. My terminal setup was powerful but had a learning curve. Cursor just works for the rest of the team without requiring them to learn zellij keybindings or configure a bunch of tools.

Parallel agents for isolated fixes

A surprising unlock for me is running multiple agents in parallel. I’m not trying to parallelmaxx or anything like that. But it’s useful to have one agent working on a main task while I spin up another to fix a few isolated bugs.

It’s the difference between context-switching manually and letting the tool handle the separation for you. When those bugs are truly independent, this feels like a superpower.

The embedded browser changes everything

But honestly? The embedded browser might be the biggest win. Being able to select specific elements on a page and chat directly about them is so much more convenient than the old workflow of describing what I want, pasting screenshots, copying links, and hoping the AI understands the context.

Pointing and clicking wins every time.

I’m not saying Cursor is perfect or that everyone should switch. But for me, it’s hit a sweet spot: powerful enough to be genuinely useful, but restrained enough that it doesn’t try to reinvent my entire development environment.

Building an Agent-Powered Climbing App

I started building a new app yesterday to better understand how agents work. The app is for climbing training, which I’ve kicked around for years. Previously, I always got blocked due to a mismatch between what I wanted and what I could build. The issue was the complexity of the data model. I felt I needed to model all the content very explicitly, which had the side effect of making the UI very heavy and inflexible. My vision was to have an experience that feels more like a text journal or flexible spreadsheet than a locked-down CRUD app.

This time I took a different approach and kept the content modeling very basic and loose. For example, a set is stored as a string: 2x 15# rather than individual columns in a table. The benefit here is that I can accommodate lots of different protocols with no extra effort. This is great since climbing training is quite nuanced.

Instead of deterministic functions to manage this information, I’m using inference — creating tools and directives that can perform analysis and transformations. While I’m ceding some predictability to the model, honestly, it seems to work pretty damn well. The other tradeoffs are speed and cost, which aren’t a concern for me with this project.

Three Principles for Prototyping with AI

I’ve been creating design prototypes with Claude Code for a while now, and I’ve noticed some patterns that make my experience much smoother. These aren’t your typical “write a detailed spec first” approaches, which are also a big part of my workflow.

Rather, think of these like like knolling or mise-en-place: ways to prepare your workspace so that you can iterate quickly without creating a tangled mess.

1. Build Components in Isolation First

It’s tempting to build features directly into the page where they’ll live. But this often leads to components that are entangled with their surroundings, making them hard to modify or reuse.

Build standalone components first, independent of where they’ll eventually go.

Think of it like designing a button in Figma before placing it in your mockups. You’re defining the component’s basic contract: what information does it need? What actions can it perform? What does it look like in different states?

This approach has three benefits:

  • Your components stay flexible as you iterate—they don’t break when you change the context around them.
  • When working with AI, you can refer to components by name and file path, making your instructions much more precise.
  • You can easily test them in isolation. I like to make component sandbox pages where I can play with properties.

2. Separate Your Data from Your Interface

When your data and interface are mixed together, it’s hard to understand either one clearly. Changes become risky because you’re not sure what you’ll break.

Put your data in a separate file (like JSON) and create a “provider” component that loads it and makes it available to your interface.

This is similar to how you might create a content document separate from your design file. The structure of your information is independent from how it’s displayed.

The benefits:

  • You understand your data structure before you commit to a visual design.
  • You can change data or interface independently.
  • You can easily test edge cases by creating variations of your data.

3. Use State Machines to Stay Organized

When you’re rapidly building something, it’s easy to end up with confusing code where you can’t tell what’s happening at any given moment. Is the form submitting? Has it already submitted? Is it showing an error? This is sometimes called “boolean soup” when states are overlapping and implicit.

State machines help you explicitly define all the possible states your interface can be in, and the rules for moving between them.

Think of it like a flow chart for your interface. Instead of having vague conditions scattered everywhere, you define:

  • All possible states (loading, success, error, idle)
  • What actions can happen in each state
  • Where those actions lead

This is especially powerful when working with AI because it gives you precise vocabulary. Instead of saying “when it’s loading,” you can say “in the submitting state.” The AI understands exactly what you mean, and you both stay aligned as you iterate.

Wrapping up

These three principles create a foundation that’s easy to iterate on. They help you stay organized even when you’re moving fast, and they make it easier to communicate precisely with AI tools about what you want to build.

Zooming out, all of these approaches increase the specificity of language. They result in unambiguous terms and separated concerns, which makes prompting and context management more precise.

Voice as an Interface with SuperWhisper

I think I’ve flipped my opinion on voice as an interface. For the past week or so I’ve been trying out Super Whisper on my new laptop. I’m astounded at the quality of transcription and the ease of use that it offers. Most importantly, it’s very tolerant of pauses in speech. I sometimes need to collect my thoughts midway through a sentence. Siri, by comparison, tends to interpret pauses as completions of thought.

Mapping a mouse button to trigger SuperWhisper was a huge unlock. This makes it convenient to dictate on the fly, without getting into a different mode.

There are some points of friction though. Super Whisper often adds a space before each sentence. Other times it doesn’t add spaces between sentences. So, I have to do a bit of review and cleanup each time.

But overall, that feels like a small price to pay for being able to get ideas out of my head a lot faster. In the past I would often journal using brief bullet points that were inscrutable a week later. I could see this becoming a convenient way to build a work diary, or even just getting more comfortable expressing my ideas verbally.

Jot: A Menubar Notepad with MCP

Recently, I’ve been hacking on Jot, a notepad that lives in your menubar. It’s a place to quickly take a note and quickly get back to the task at hand.

One fun thing I added was an MCP server. Truthfully, I didn’t have a strong use case for it just yet — I wanted to build it and see what it inspired. The video shows the create_note tool being used by Claude Desktop.

Already, I found a few interesting uses for it. For example, I wanted to create a note explaining what Jot does. I had Claude Code scan the repo, summarize the functionality, and create a note explaining how to use it.

Welcome to Jot screenshot
Welcome to Jot screenshot

Planning Dark Mode with OKLCH Colors

Today’s work involved dark mode planning. We need more colors at the ends of the spectrum to make this work. While I was at it, I figured I’d clean up some design debt by snapping the colors to precise, similar lightness values.

Color palette in Huetone
Color palette in Huetone

Working in oklch color space is wonderful for this. I think it’ll be essential for supporting user-generated themes, as it allows us control over contrast at runtime.

Generating Color Palettes Algorithmically

While working on Whimsical Dark Mode, I needed to expand our range of colors at both ends of the spectrum. Previously, this is something I’d do manually, but there is one big catch: we allow users to create custom palettes.

As a result, we need an algorithmic way to generate color palettes, since we can’t control what they choose at runtime. Given a single color, we need to infer a harmonious range of colors.

From previous work, I knew the oklch color space was the best bet.

  • The lightness channel would form the basis for steps. A constant L value across colors ensures they have perceptually similar contrast. We also want to offer more colors at end ranges for layering and nuance.

  • The hue channel would be chosen by the user. We’d infer this from a user’s choice.

  • The chroma channel was the tricky part. We want to desaturate the colors as the approach the ends, so that they don’t look too vibrant. We’d also infer this from the user’s choice.

To achieve this, I spun up a Make app that lets me generate palettes parametrically. This lets me try out different easing functions for L and C channels before moving into code.

OKLCH palette generator in Figma Make
OKLCH palette generator in Figma Make