SLOPSHOPPER

cc-mod-input-reset-repro

Repro: a Claude Code mod Input drops the newest typed character when a redraw draws a stale value

panecommandtimer
v0.1.0MITupdated 2026-10-16bassic-nerd/cc-mod-input-reset-repro
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cc-mod-input-reset-repro
│ ┃ Input reset ✕ › fix the failing auth test and add an audit log call │ ┃ Type, at normal speed: the quick brown fox │ ┃ jumps over the lazy dog ⏺ Read(src/auth.ts) │ ┃ type here ⎿ Read 6 lines │ ┃ typed 0 chars: ok, still a prefix of the ⏺ Update(src/auth.ts) │ ┃ target ⎿ Added 2 lines, removed 1 line │ ┃ [ workaround (constant seed): ] ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /input-reset │ ⎿ cc-mod-input-reset-repro: Input reset pane opened. Type the targ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Input reset
Type, at normal speed: the quick brown fox jumps over the lazy dog type here typed 0 chars: ok, still a prefix of the target [ workaround (constant seed): ]
README

cc-mod-input-reset-repro

A Pane with an Input drawn with value={state.text}, updated in onInput, plus a 200 ms timer that invalidates ui.render (as a spinner or a live poll would). A redraw that lands between a keystroke and the mod hearing about it draws the older value, and the host appears to discard its edit buffer, so the newest character vanishes.

Environment

  • Claude Code 2.1.300 (still present in 2.1.302), terminal surface
  • macOS; seen in Zed's integrated terminal
  • Mod API: ui.render hook drawing a Pane with the Input element

Install and run

From a terminal session, at the prompt:

/plugin install cc-mod-input-reset-repro --marketplace bassic-nerd/cc-mod-input-reset-repro

Answer y to add the marketplace, then pick a scope. Or load it straight from a clone, for one session:

git clone https://github.com/bassic-nerd/cc-mod-input-reset-repro
claude --plugin-dir ./cc-mod-input-reset-repro

Then run the slash command /input-reset. It opens a pane and gives it the keyboard.

The whole mod is one file, hooks/register.tsx.

Repro steps

  1. Run /input-reset. The pane shows a target sentence, an Input, a status line and a workaround toggle (off).
  2. Type the target sentence at normal speed: the quick brown fox jumps over the lazy dog. Do it a few times; clear the field between attempts by pressing the toggle twice.
  3. The status line reads ok while what you typed is still a prefix of the target, and MISMATCH at N once a character is missing.
  4. Press the toggle to turn the workaround on and repeat the typing.

Expected

Every typed character lands in the field, in order, whether or not the mod redraws meanwhile.

Actual

  • With the workaround off, the status line occasionally flips to MISMATCH: the newest character was dropped. It does not appear in the main prompt; it just vanishes.
  • It is intermittent and more likely at a normal-to-fast typing speed.
  • With the workaround on, the same typing does not drop characters.

What the toggle changes

  • Off (bug): the Input is drawn with value={state.text}, the value the mod last heard. Between a keystroke and onInput running, a timer redraw draws the previous value.
  • On (workaround): the Input is drawn with a constant value="", a seed that only changes programmatically (the toggle also gives the Input a fresh key). A redraw never carries a different value, and the typed text survives. The status line is still driven by onInput.

So value is not only a seed: drawing a changed value appears to replace what the person has typed. The types describe value as "the text the field holds when drawn; the person's typing replaces it until the hook draws another", which allows this reading, but it makes a lagging value a data-loss hazard.

Checks

claude plugin validate .
claude plugin test .
tsc --noEmit -p .

(the last needs the API types in .claude-plugin/types/, which Claude Code lays beside a mod it loads from a folder.)

License: MIT.

Source 1 files
hooks/register.tsx 70 lines
1import type { Register } from 'claude-code'
2
3const PANE = 'input-reset'
4const TARGET = 'the quick brown fox jumps over the lazy dog'
5
6// Module state is enough for a repro; a hot reload resets it.
7const state = { text: '', workaround: false, epoch: 0, timer: false }
8
9// First index where `text` stops being a prefix of TARGET, or -1 while it still is one.
10export const mismatchAt = (text: string): number => {
11  for (let i = 0; i < text.length; i++) if (text[i] !== TARGET[i]) return i
12  return -1
13}
14
15export const register: Register = on => {
16  on('session.start', async ($, e, next) => {
17    await $.command.register({ name: 'input-reset', description: 'Open the Input reset repro pane' })
18    return next(e)
19  })
20
21  on('command.run', { command: 'input-reset' }, async $ => {
22    await $.ui.open({ id: PANE, title: 'Input reset', focus: true })
23    if (!state.timer) {
24      state.timer = true
25      // A redraw every 200 ms, as a spinner or a live-refresh poll would cause.
26      $.clock.every(200, () => $.ui.invalidate('ui.render'))
27    }
28    return { text: 'Input reset pane opened. Type the target sentence at normal speed.' }
29  })
30
31  on('ui.render', { component: 'Pane', requestId: PANE }, ($, e, next) => {
32    if (e.surface !== 'terminal') return next(e)
33    const { Box, Text, Input, Button } = $.ui.resolve(e)
34    const bad = mismatchAt(state.text)
35    // Bug mode draws the value the mod last heard. Workaround mode draws a seed that only
36    // changes programmatically (the toggle), so a redraw never carries a different value.
37    const value = state.workaround ? '' : state.text
38    return (
39      <Box flexDirection="column">
40        <Text dimColor>Type, at normal speed: {TARGET}</Text>
41        <Input
42          key={`text:${state.epoch}`}
43          autoFocus
44          placeholder="type here"
45          value={value}
46          onInput={v => {
47            state.text = v
48            $.ui.invalidate('ui.render')
49          }}
50          onSubmit={() => {}}
51        />
52        <Text>
53          typed {state.text.length} chars: {bad < 0 ? 'ok, still a prefix of the target' : `MISMATCH at ${bad} (a character was dropped)`}
54        </Text>
55        <Button
56          key="toggle"
57          onPress={() => {
58            state.workaround = !state.workaround
59            state.text = ''
60            state.epoch++
61            $.ui.invalidate('ui.render')
62          }}
63        >
64          workaround (constant seed): {state.workaround ? 'on' : 'off'}
65        </Button>
66      </Box>
67    )
68  })
69}
70