DEV Community

Haruki Nakada
Haruki Nakada

Posted on

Claude Code mod button calling $.prompt.submit doesn't work the first time (where to call it)

If you call $.prompt.submit inside a mod button's onPress or inside a command, Claude sometimes doesn't start. In the versions I tested, a $.prompt.submit called inside the press event was thrown away when the event ended, with no error. Have the button only store what to send in a variable, and send it from a clock created in session.start. That works reliably.

Versions checked: Claude Code 2.1.286 to 2.1.288 (October 2026, desktop Code tab).

Symptom

Put a button in a mod's panel, and when pressed, send a request to Claude. It's the first thing you want to build.

t.Button({
  key: 'ask',
  label: 'Decide now',
  onPress: async () => { await $.prompt.submit({ text: 'Check the market and decide' }) },
})
Enter fullscreen mode Exit fullscreen mode

Pressing it doesn't start Claude. Sometimes pressing twice works, which made the cause slow to find. Sending from inside a command (/xxx) behaved the same.

What the official docs say

On the official API page, $.prompt.submit sits in the section about starting a turn from background work. Three key points:

  • $.prompt.submit({ text }) waits until the session is idle, then starts a new turn
  • It resolves when the turn starts, so don't await it inside a handler that runs while Claude is working
  • Claude reads the text after a line saying your mod sent it. With asUser: true, it's sent as the person's own words without that line

The example given also sends from inside a clock ($.clock.every) created in session.start.

What I observed

The official text goes as far as "it waits, so don't await it". It does not say that a call inside an event gets thrown away. What follows is what I saw in my versions.

  • A $.prompt.submit called inside a button's onPress or a command hook disappeared when the event ended, and no turn started
  • No exception, and nothing reached .catch
  • Moving the same call into a clock made it work every time

Why it gets dropped is not verified. But together with the fact that the official example sends from a clock, I believe the right place to start a turn is a clock, not an event.

Fix: the press only queues, the clock sends

The button only puts "what to send" into a variable, and a 1-second clock finds it and sends it.

let toSend = null

// Button: just queue it
onPress: () => {
  toSend = 'Check the market and decide'
  $.ui.toast('Sending...')
}

// Create the clock once in session.start
on('session.start', async ($, e, next) => {
  $.clock.every(1_000, () => frame($))
  return next(e)
})

async function frame($) {
  if (!toSend) return
  const text = toSend
  toSend = null            // clear before sending (so the next tick doesn't send it twice)
  await $.prompt.submit({ text })
}
Enter fullscreen mode Exit fullscreen mode

There's up to a 1-second delay between pressing and sending, but you barely notice it. Show "Sending..." in a toast or the panel right after the press so there's no doubt whether the press registered.

frame takes $, so put it at the top level of the same file. If you pass $ to a function created inside a hook or to a function in another file, the static check fails and the whole mod doesn't load (part 5).

For the same reason, start external processes from the clock

Work that starts an external process with $.process.run (opening a file in the default app, playing a sound) was also sometimes killed when the event ended if started inside the event. Route that to the same clock too. Processes started from the clock keep running beyond the event.

Remember "any work that outlives a single event starts from the session.start clock" and you won't get lost. The official docs also describe running work that spans more than one event on a clock started in session.start.

Keep only one clock

If you create a clock per feature, you hit a different trap. In my versions, writing settings ($.config.set) reloaded the plugin and ran register again, so clocks multiplied and the same work ran twice.

The official explanation says that on reload the previous clocks stop and the new load creates its own. It may be fixed in newer versions, but which version changed it is not verified. To be safe either way, I follow two rules:

  • Only one place in the mod owns the clock
  • Stop the previous clock before creating a new one
let timer = null

function startClock($) {
  try { timer?.cancel?.() } catch { /* do nothing if there isn't one */ }
  timer = $.clock.every(1_000, () => frame($))
}
Enter fullscreen mode Exit fullscreen mode

The clocks returned by $.clock.every and $.clock.after have cancel() (official).

Re-rendering too fast recreates the button you're pressing

There was one more cause of "I pressed it but nothing happened". If you re-render the panel every second, the button can be recreated mid-press.

  • Only animated graphics every second
  • The normal view every 5 seconds

Splitting it like this fixed it. Calling $.ui.invalidate('ui.render') only when a value changes cuts wasted re-renders in the first place.

How to word what you send

Text sent with $.prompt.submit looks to Claude like "a request that came from a mod".

  • Normally don't set asUser. Claude reads it knowing it came from a mod
  • Use asUser: true only when you want it treated the same as something a person typed

A mod that asks Claude something with one button can also start turns without the person knowing. From the installer's side, where a mod calls $.prompt.submit from is one of the things worth checking (part 10).

Summary

Symptom Cause Fix
Asking from a button or command doesn't start Claude $.prompt.submit inside the event was dropped (in my versions) Queue in a variable, send from the session.start clock
Opened apps or sounds get cut off Processes are killed when the event ends Start them from the clock as well
The same work runs twice Clocks are created in more than one place One clock, stop it before creating a new one
The press feels registered but nothing happens Re-rendering recreated the button Re-render every 5 seconds

You can read real mods that ask Claude from a button, with their code, in the modscode reviewed gallery. Comparing how they create clocks and place buttons shows the patterns clearly.


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)