2026-01-07
I’m trying to test Claude Code by having it build the Spatterpad interface, with dragging to reorder AND dragging to combine.
I had it build a basic interface with dragging to reorder. It used dnd-kit, but when I implemented dragging to merge, it introduced a bug where if you drag a bubble horizontally, it removes it from the DOM.
I tried to fix it using Claude Code (Opus 4.5). I explained the issue and it attempted to fix it. The bug remained. I told it that the issue was still there, and we repeated the process. Each time, Claude Code made changes to the codebase, but the issue was still there.
I ended up resetting the code to the last commit and trying to debug it myself. It was a one-line fix; I spent much more time getting Claude Code to work on it then to do it myself.
When migrating to TypeScript, the dev server obviously works, but Claude Code thought it didn’t, running a bunch of test commands, until ultimately concluding that it does work.
⏺ Background command "Test dev server starts" failed with exit code 137.
⏺ That's expected - exit code 137 means the process was killed (by my KillShell command). The dev server was working correctly before I terminated it - it served the index.html successfully as shown in the output I read earlier.2026-01-09
When giving the same prompt to Cursor and Claude in plan mode, Claude asked me the crucial clarifying question, whereas Cursor chose the simplest option (when merging, tell dnd-kit the collision is happening at the target position). I’ve told Claude to ask me clarifying questions, though, while Cursor is on the default behavior.
In the end, neither were right: it was the target position when dragging down, and the last position when dragging up.
Some helpful git prompts:
Compare the main branch to the tuplets-again branch
Delete all merged branches
2026-01-10
I asked to implement Playwright in the Spatterpad app, and it did it swimmingly. When I asked it to implement tests for the merge preview behavior (which happens when you’re dragging over the merge zone, but having not yet let go), it seemed to generate an appropriate plan except for one line:
Tests verify merge completion rather than mid-drag state for reliability
This completely undermines the purpose of the test. I didn’t read the plan closely enough, so I missed this, and it implemented merge behavior, rather than merge preview behavior.
I corrected it:
The test you've just added seem to be testing merge behavior, when I asked to test merge PREVIEW behavior. Merge preview behavior can be seen during drags over the merge zone BEFORE letting go (completing the drag).
And it corrected the plan, including that line:
Tests verify the PREVIEW state mid-drag, not merge completion
One of the checks for merge preview behavior is making sure that no bubbles overlap during a merge preview. Initially it did this by comparing the bounding box for all the bubbles. This wasn’t accurate, though, because we set the height of the target element to 48px (so that it doesn’t jitter the sorted list), even though it appears taller in the DOM. Claude was not able to recognize this at first. Here’s how we fixed it:
Me: “Is this overlap logic accounting for transforms?” (thinking this might be the issue)
Claude (excerpt): “(checks with some console.logs) The boxes don't overlap - each 48px tall bubble has clear separation. The transforms are being accounted for since boundingBox() returns the actual screen position.”
Me: The thing is, when we're in the merge preview, the target bubble should not be 48px high. Yes, in the css we're SETTING it to 48px, but on screen it's actually higher than that. How do we test for THAT visual height?”
Claude: You're right. Looking at NoteBubble.tsx, during preview the outer wrapper has:
height: showPreview ? originalHeight ?? "auto" : "auto",
overflow: showPreview ? "visible" : undefined,
So
boundingBox()returns the CSS box (48px), but the inner content visually overflows. We need to check the inner div's bounding box instead - that's where the actual visible content is.
And then Claude checked the inner div’s bounding box instead and reran the tests, finding that one of them failed, as it should.
When I asked Cursor (Opus 4.5) to refactor a component, it put the component styles in a useMemo, which didn’t seem necessary. So I asked it:
Me: Why is the style in a memo?
Cursor: Good question — honestly, it's unnecessary here. The style object is just a simple computation with primitive values. useMemo would only be worthwhile if:
- The computation was expensive (it's not)
- The object reference stability mattered for a child component's props (it doesn't — it's just a DOM element's style prop)
React already handles inline style objects efficiently for DOM elements. The memo adds cognitive overhead without real benefit. Want me to remove it and just use a plain object?
As a general point, AI seems to refactor without any taste, but it can be good at explaining things and can refactor specific areas that you tell it to. Here’s an example of a refactor I made, cleaning up AI code. -40 lines. Most of it was me, but I used to it to install and use clsx, and I used to it to help me understand and ultimately simplify the hidden state of the NoteBubble.
When asking Cursor (Opus 4.5) to make a GitHub repository for the project, it did, but it made the current branch the default branch instead of main.
2026-01-11
I find that AI helps keep me in the flow and reduce friction. If I’m ever stuck, AI takes the next step for me, and gives me my next step: reviewing the code.
Tried to get the Claude code extension to configure a domain on NameCheap to be used with GitHub pages. It completely failed, spending 5+ minutes to ultimately just change Custom DNS → NameCheap basic DNS (which is indeed the first step). To be fair, it’s using Haiku, but there was no option to change the model.
Tried to get the Claude code extension to record my process of correcting my application on a myworkdayjobs site. It stored that specific one as the starting site, so when I tried to use it on another, it went to the first site. I could manually change the starting site, but that still refreshes the page, which loses the start state I need.
2026-01-12
I wanted to share MCPs between Claude Code and Cursor. I’m sure there’s a smart way to do this, but for now I just asked Claude code to configure cursor to use the same MCPs it had access to.
I tried to get Opus 4.5 to build an Obsidian remark plugin that could handle transclusions (![[note]].
This did not go well; it created some sort of infinite loop and took 15 minutes to fix it. But then we had another issue—only one of the two test transclusions worked. It took another 6 minutes to figure that out. It was an issue with curled vs straight apostrophes and I’m sure it would’ve taken me a lot longer than 6 minutes if I were to debug it myself.
The feature ultimately worked! The problem is that now I have a huge file that’s in a domain I don’t have experience with. It’s easier, though, to learn enough to review it than to learn enough to create it.
2026-01-13
Okay, the feature has an issue. Wikilinks don’t work within transclusions, with the raw Obsidian syntax being rendered. I tried to get Claude to fix it, twice, using 77% of my 5 hour usage limit, and it failed; the first time leaving things looking exactly the same, and the second time rendering the text of the link, without making it a link.
As an experiment, I undid everything Claude tried, and used Cursor (with 33% of my monthly usage left after yesterday) to run GPT 5.2. It solved it in one line, but this fix only worked in build mode. (It’s possible that the Claude fixes also worked in build mode and I just didn’t realize, but it’s wild to me that Claude’s fix was so large compared to GPT.)
Twice it tried to get it to work in Dev mode, wrote a bunch of code, told me it worked in dev mode, but it did not. The second time, it broke the dev server entirely—it got stuck on Syncing content until it crashed (JavaScript heap out of memory). After several attempts, it could not fix this.
I love GPT 5.2 hyping itself up:
It seems the server might not be using port 4334 because that port is already in use, so it probably chose a different one. I’ll need to check terminal 4 to find the actual local URL. I’m excited to dig deeper and see what’s happening! It’s a bit of a puzzle, but I can unravel it with a little more investigation.
Cursor eventually used up 133GB of memory (I don’t have that much memory but that’s what Activity Monitor said) and crashed my computer.
I decided to start over. The way I prompted it initially was to start minimally, then build up through follow up prompts. From my first plan file:
Prompts
My Obsidian vault is located at /Users/danjutan/Documents/Vault. Configure content collections to pull every file in my vault that has the frontmatter attribute publish: true
Follow-up Prompts
It doesn't look like we're preserving newlines
Fixing line breaks is one thing, but I want to make sure our import is compatible with as many Obsidian features as possible. Tables, pull quotes, callouts, wikilinks, transclusions, etc
This time, I was more comprehensive in my initial prompt, and I included the transclusion issue ahead of time:
My Obsidian vault is located at /Users/danjutan/Documents/Vault. - Configure content collections to pull in every file within the Knowledge folder that has publish: true, or is linked to by a file with publish: true - Images must work, pulling them into the asset directory - Bold, italics, quotes, lists, headings, and other Obsidian formatting must work - Wililinks e.g.
[[Note]]and transclusions e.g.![[Note]]must work - CRUCIAL: Transclusions must render the transcluded part of the note in the same way as any root note. e.g., all wikilinks inside a transclusion must work. Nested transclusions must work - Do not write any CSS at this point
And it worked in one go (that took 95% context and 51% of my 5 hr usage)!
2026-01-14
If you delete a bunch of files, Claude will still see it. It sees the git diff and git history.
I created a custom Cursor user rule
Load ~/.claude/CLAUDE.md and listen to it, every time
Because the “include CLAUDE.md in context” wasn’t working
2026-01-19
There was an old TODO in my Songshed project, in the SelectionRegion.vue file:
const startColumn = computed( // TODO: why did we have to change this from +1 to +2 () => (props.region.minPosition - tabBarBounds.start) / subunit.value + 2,);
const endColumn = computed( () => (props.region.maxPosition - tabBarBounds.start) / subunit.value + 3,);I asked both Claude Code and Cursor (GPT-5.2 Codex) to investigate this TODO. They both came back with the same idea: that the increase was due to the additional widget column. Bars that have widgets have an additional column inserted before the stacks, so we need to add 1 to whatever the stack index is. The AI said that this code breaks for bars that don’t have widgets, so we need to inject a hasWidget state to fix that.
The AI did not consider the deeper problem, which is that all of my bars have widget columns, even if they don’t have widgets, which is why there’s no actual bug here, just a confused TODO. The real problem is in the GuitarBar.vue file, where I conditionally create a column if there is a widget:
const hasWidget = computed(() => slots.widget);This is always true; it doesn’t actually check the contents of the slot.
So AI debugging pointed me to the right direction of the issue, but did not find it itself.
2026-01-20
After I fixed the above issue—so there are no empty widget stacks—I expected the selection logic to fail on bars without widget stacks. The selection placement adds 1 to account for the widget stack, but those bars don’t have one.
I asked Claude Code to use the Chrome Dev MCP to figure out what’s going on.
I’ve noticed when using this MCP that Claude will always try to interact with the keyboard first, which makes sense because that’s an easier interaction to automate than mouse interactions. In fact, it couldn’t figure out how to drag from one cell to another to make a selection. It asked me to do so, so it could inspect it.
The reason why the selection seemed to work is because the SelectionRegion code was determining the placement of the selection hover toolbar, not the visual selection itself. I didn’t realize this because I was just jumping into a codebase I hadn’t touched in 9 months, but you’d think AI would be able to read it cohesively and understand. Asking Claude straight up where the visual selection rendering is, it told me the wrong place (it’s in NoteContainer, not SelectionRegion).
I would say my naming is misleading; I would say there’s an issue with name a component SelectionRegion when the visual logic is elsewhere. That said, it’s telling that Claude infers behavior from the naming of one file but misses functionality elsewhere in the codebase. It does read NoteContainer, but it only files it under Event Handling; it doesn’t see the .selected CSS. Meanwhile, if you look at the template and CSS of the SelectionRegion, you can see that all it does is render an empty div and a toolbar. I grant that I made the same mistake (and I’ll be renaming these files) but the point of AI is to not make the same mistake as humans.

I’ve always wondered whether I should’ve gone with Tailwind for Songshed. We use a lot of dynamic styles so I figured raw CSS was the way to go, but those could always be the exception in a Tailwind setup. I decided to ask Claude to migrate the app to Tailwind in a new branch.
In its plan, it identified what couldn’t be converted to Tailwind:
Styles That CANNOT Migrate to Tailwind These must remain as scoped CSS:
- v-bind() reactive bindings (25 instances across 12 components)
- Dynamic grid templates (e.g., grid-template-columns: repeat(v-bind(numStacks), ...))
- Container queries in NoteContainer.vue and PositionLine.vue
- SVG fill/stroke bindings
Its first pass broke the visuals and still used a ton of CSS (and a ton of @apply which is not the most idiomatic Tailwind). It also (including planning) took 71% of my 5 hour usage so I decided to put it to rest for now.
2026-01-22
I gave Claude Code a highly DOM-involved task:
I want the bar divider to extend up into the annotation rows (if any exist). Annotations can still be created and extended across bars. If an annotation spans across bars, it should occlude the bar (that includes during dragging). In such a case, the bar should only be occluded on the annotation row(s) that has the cross-bar annotation. If there's no annotation across a bar line, the divider should be visible.
This should use the existing BarDivider, so we get all of our hover and control logic. Do not create new divider elements. The BarDivider should be hoisted up and be in between TabBars.
It completely failed; the bar dividers are gone except for one before the tab even starts:

I gave the same prompt to Cursor (GPT-5.2 Codex). At first, I forgot to turn on Plan mode. This is what I got; the bar divider completely disappeared, leaving a gap:

I ran it with Plan mode and got the same result. After taking a screenshot and telling it that bar dividers aren’t being rendered at all, it fixed that. But there are other issues; the occlusion occurs during dragging and hover but not at rest:
Still, it was a good starting point for me to review and fix.
2026-01-26
I had a layout shift issue of a few pixels—after clicking the title edit button, the chord diagram would shift a few pixels to the right. I sent Cursor two screenshots, one before clicking, one after (it’s hard to tell the difference when you aren’t actually clicking on it):
![]() |
![]() |
|---|
Cursor got stick in some kind of permissions issue where it couldn’t edit the right file. Claude Code quickly identified and solved the problem, saving me a CSS headache.
I mostly vibe-coded a scrolling picker by pointing Claude Code at this article and asking it to implement it in our codebase using our styles. I then created a chord select interface out of it, also mostly vibe-coded:

2026-01-28
I find when working on resume and cover letters, ChatGPT is too opinionated, Gemini isn’t opinionated enough, but Claude seems to be a good middle ground.
2026-01-29
When refactoring the chord select Vue code, I notice Claude used a lot of watch (the equivalent of useEffect in React, which is frowned upon for being imperative in an ideally declarative framework). In the future I might want to experiment with getting it refactor those, but for now I wanted to do it by hand as there were other issues with the code—nothing that affected its functionality, just things that could be streamlined and done in less code, and things that weren’t in my style.
It also didn’t use useTemplateRef, a relatively new feature in Vue.
Claude set pointer events on the window to implement scrolling the picker by dragging with the mouse. Cursor suggested refactoring to use setPointerCapture, a new JavaScript feature. It made the code more complex so I left it out for now, but left a comment for later.
Cursor bugbot is great. It summarizes PRs wonderfully and it picked up on a bug in Claude’s code.

That said, after fixing some of the bugbot’s bugs, it rescanned the codebase and found bugs that were there before, so it isn’t necessarily comprehensive.
2026-01-30
In my danjutan.com project (which is almost completely vibecoded), Claude Code introduced mee to the scrollbar-gutter property. With scrollbar-gutter: stable the browser creates space for the scrollbar even if it isn’t there, so navigating from a page with a scrollbar to one without doesn’t cause layout shift. Without AI, I probably would’ve tried to solve this another way and perhaps would never discover the property.
2026-02-02
I used Claude Code and Claude web to proofread my A Taste of Prolog article. At first it mostly found grammatical errors but then I told it to read for technical accuracy. Then, each time I ran Claude, it found more errors. I wish I could trust it to be comprehensive.
I had AI make an interface based on an Excalidraw drawing:

2026-02-04
I had a bug in Songshed where downswings weren’t rendering if the bend note was at the start of the tab.
I explained the issue to Claude:
In most cases, you can make a prebend, then drag from the rightward facing arrow that appears next to it to drag down a downswing. However, this behavior is broken if the note we're bending is on the first note of the tab. investigate.
And it found a one-line fix; I was using the position as a boolean, causing it to be false when the position was 0. I could’ve found this myself, but it would take a lot of reading to reacquaint myself with the code and find the bug. I wouldn’t know whether to look at the bend rendering code or the bend state code. Claude one-shotting bug fixes is incredibly satisfying.
| Broken | Fixed |
|---|---|
![]() |
![]() |



