DEV Community

Haruki Nakada
Haruki Nakada

Posted on

Claude Code mod works in the terminal but breaks in the desktop app (pane not opening, Svg shrinking, flicker)

When a mod that works in the terminal is opened in the desktop app, these five problems come up most: a pane opened at startup does not appear (opens without a user action need 144 columns), hex colors are unreadable in the light theme, Raster is not drawn, Svg shrinks to 300x150, and an isInteractive Svg flashes black every second. All of them can be fixed by branching on e.surface.

Tested with Claude Code 2.1.286 to 2.1.288 (October 2026, Windows, 150% scaling).

1. A pane opened at startup does not appear

Even if you call $.ui.open(...) in session.start, the pane may not appear on the desktop. The official interface page has rules for this:

  • An open backed by a user action (a command, a pressed button, a sent prompt) is placed regardless of width
  • An open without one (from session.start, a timer, or turn.start) needs 144 columns. A pane the user has opened themselves at least once needs 110 columns
  • When there is not enough room, $.ui.open returns { isPlaced: false, reason } and waits

The desktop side panel is narrow, so assume an open at startup may not be placed. I chose to reopen on the first prompt submission.

let opened = false

on('prompt.submit', async ($, e, next) => {
  const kind = e.origin?.kind
  if (!opened && (kind === 'composer' || kind === 'bridge')) {
    opened = true
    await $.ui.open({ id: PANE, title: 'My Mod' })
  }
  return next(e)
})
Enter fullscreen mode Exit fullscreen mode

The values of e.origin.kind (composer, bridge) are what I saw in the type definitions for my version. On other versions, check the type definitions (.claude-plugin/types/ from Part 2).

Other options are to open the pane from a command or a band button, or to show a toast saying it can be opened instead of opening it on your own (Part 4).

Also, the desktop app I tested (2.19675) accepted rows and columns in $.ui.open but did not use them for the pane size. Draw on the assumption that the size is whatever width the user dragged it to.

2. Hex colors are unreadable in the light theme

In the terminal you can paint with color: '#7FD1A6'. On the desktop, hard-coded colors can stand out oddly or become unreadable in the light theme. Pass theme color names instead and they render correctly in both light and dark.

Meaning Name
Up, success success
Down, removal diffRemovedWord
Warning warning
Dim text dimColor: true

The official reference for Text also says color is a theme key or a color like 'red'. If you keep hex values in your feature code and put a single wrapper that swaps them only on the desktop, you do not have to rewrite anything.

const THEMED = { '#7fd1a6': 'success', '#f2837f': 'diffRemovedWord', '#d9b26a': 'warning' }

function themeProps(props) {
  const out = { ...props }
  const v = typeof out.color === 'string' ? out.color.toLowerCase() : null
  if (v && THEMED[v]) out.color = THEMED[v]
  return out
}
Enter fullscreen mode Exit fullscreen mode

3. Raster is not drawn, or the surface is detected wrong

A mod that draws a grid of colored cells with Raster in the terminal cannot draw it on the desktop. Raster and Image are terminal-only, and Svg is desktop-only.

If you try to detect the surface by checking whether a component exists, like if (t.Raster), you will get it wrong, because the table from $.ui.resolve(e) contains every component name. Always branch on e.surface ('terminal' or 'desktop').

if (e.surface === 'terminal') {
  return Raster({ key: 'grid', columns, rows, cells })
}
return Svg({ source: svgOf(grid), alt: 'Heatmap', width: w, height: h })
Enter fullscreen mode Exit fullscreen mode

4. Svg shrinks to a small box

If you do not pass width and height to Svg, the frame stays at the default 300x150 (CSS pixels), and the picture shrinks to fit inside it. This happens even if the SVG itself has a viewBox.

t.Svg({ source: svg, alt: 'Chart', width: w, height: h })
Enter fullscreen mode Exit fullscreen mode

The catch is that the pane size reaches the mod only in columns and rows (bodyColumns, bodyRows), not in CSS pixels. On my screen, one pane cell measured 7.6 wide and 18.6 tall (CSS pixels).

const CELL = { w: 7.6, h: 18.6 }
const w = Math.floor(cols * CELL.w)
const h = Math.floor(rows * CELL.h)
Enter fullscreen mode Exit fullscreen mode

These values may differ on other setups, so make the drawing robust to a wrong estimate:

  • Give the root <svg> width="100%" height="100%" and a viewBox, so it keeps its aspect ratio and fits even if the frame differs from your estimate
  • Estimate a little small and fill the leftover space with the background color of the outer Box

source is limited to 131,072 characters (official reference). 200 candlesticks already take tens of thousands of characters. Round coordinates to one decimal place and group identical fills into classes.

5. An isInteractive Svg flashes black every second

An Svg with isInteractive: true is drawn inside a frame (an iframe), so :hover and CSS animations work. However, every time the content changes, the whole frame reloads and goes black while it does. A view redrawn every second flashes every second. I also tried stacking two frames and loading in the back one, but that did not fix it.

The conclusion: do not use isInteractive; use a single image Svg.

  • An image Svg keeps showing the old picture until the new one is ready, so it does not flash
  • CSS @keyframes written inside the SVG still run in an image Svg

What you lose is roughly :hover and <title> tooltips.

Animations restart from the beginning on every redraw

Even with an image Svg, changed content becomes a new image, and CSS animations start over. If you write the animation delay as "event time minus the time the SVG was built," the animation continues from the middle even after a rebuild, because a negative delay means "already partway through."

const madeAt = Date.now()
const delay = (eventAt) => `${((eventAt - madeAt) / 1000).toFixed(2)}s`

svg += `<text class="type" style="animation-delay:${delay(caption.at)}">${esc(caption.text)}</text>`
Enter fullscreen mode Exit fullscreen mode

Things that change every second (elapsed time and so on) should stay out of the SVG and go in a Text row above it. That reduces how often the SVG itself is rebuilt.

Colors go wrong when two are side by side

If you reference <linearGradient id="g"> with url(#g), the second of two side-by-side images points at the first one's gradient. Give each one a unique prefix (id="s3-g").

Two more things you will hit

You cannot style buttons, and clicks on top of the picture do not register

The desktop Button has a fixed look; you cannot set its color, shape, or size. HTML cannot go into a pane either. I tried drawing the whole screen as an Svg and layering a Client on top to catch where the user clicked, but clicks on a Client layered over the picture did not work (2.1.286 to 2.1.288). The layout I settled on is: visuals in an Svg picture, and standard Buttons placed above or below the picture for anything clickable. Writing plain: false makes the button undrawable on the desktop, so only use true or omit it.

Layouts break in the narrow pane

The usual way a pane opens on the desktop is the narrow side pane next to the conversation (about 58 columns, 440px). Layouts tuned on a wide screen broke here: charts collapsed into thin columns, axis labels overlapped, and legends were cut off.

  • If placing items side by side would leave a chart under 60 columns, switch to stacking them vertically
  • Drop legend entries that do not fit, starting from the end

Open it once in the narrow pane before you polish anything. That is the fastest route.

Checking an Svg without opening the app

Opening the desktop app every time is slow, so I put the SVG in an HTML file and screenshot it with headless Edge. The trick is to shoot at the usual panel width (about 440px).

& "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --headless=new --screenshot=out.png --window-size=440,600 file:///C:/work/stage.html
Enter fullscreen mode Exit fullscreen mode

Summary

Problem Fix
Startup pane does not appear Open from a user action (automatic opens need 144 columns)
Colors are unreadable Pass theme color names
Raster is not drawn Branch on e.surface, use Svg on the desktop
Svg shrinks Always pass width and height (1 cell is about 7.6x18.6)
Svg flashes Do not use isInteractive
Buttons cannot be styled Visuals in Svg, clicks on standard Buttons
Layout breaks Make the 58-column narrow pane your pass line

You can tell whether a mod written for the terminal will draw on the desktop by checking its code for an e.surface branch before installing. The modscode gallery lists only mods that explicitly target the desktop app, with reproductions of how they look in the narrow desktop panel.


This article was written with AI assistance (Claude) and checked against the official Claude Code docs; anything marked "not verified" could not be confirmed there.

Top comments (0)