SLOPSHOPPER

surface-probe

A pane that shows what this surface does before you design for it: its width and placement, which elements it draws, right-to-left text, SVG, and whether a…

newpanecommand
v0.1.0MITupdated 2026-10-04barmoshe/bar-mods/surface-probe
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · surface-probe
│ ┃ surface-probe ✕ › fix the failing auth test and add an audit log call │ ┃ surface-probe │ ┃ ⏺ Read(src/auth.ts) │ ┃ surface terminal · placement dock · ⎿ Read 6 lines │ ┃ bodyColumns 56 · viewport 160x42 · ⏺ Update(src/auth.ts) │ ┃ isFullscreen true ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ שלום, זו שורה בעברית בלבד. ⎿ 3 pass, 1 fail │ ┃ │ ┃ Mixed: המשימה t03 נפתחה ב-4.10.2026 (Claude) ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ┃ [ לסגור ] [ לנקות ] [ לתיבת ההודעה ] [ לשלוח ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ ╭──────────────────────────────────────────╮ › /surface-probe │ ┃ │ מיושר לימין │ ⎿ surface-probe: surface-probe is open. │ ┃ │ שורה שנייה, ארוכה יותר מהראשונה │ │ ┃ ╰──────────────────────────────────────────╯ │ ┃ │ ┃ no Svg on terminal │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · surface-probe
surface-probe surface terminal · placement dock · bodyColumns 56 · viewport 160x42 · isFullscreen true שלום, זו שורה בעברית בלבד. Mixed: המשימה t03 נפתחה ב-4.10.2026 (Claude) [ לסגור ] [ לנקות ] [ לתיבת ההודעה ] [ לשלוח ל-Claude ] שורה ╭──────────────────────────────────────────────────────────╮ │ מיושר לימין │ │ שורה שנייה, ארוכה יותר מהראשונה │ ╰──────────────────────────────────────────────────────────╯ no Svg on terminal
README

surface-probe

A pane to open before you design a big UI for a surface. It shows what that surface really does, so the plan starts from facts instead of guesses.

  • What the surface reports: surface, placement (docked beside the transcript or above the prompt), body width in cells, viewport.
  • Right-to-left text: a Hebrew line, a mixed Hebrew and English line, a row-reverse row and a right-aligned box. The terminal draws Hebrew in reverse; the desktop draws it right.
  • SVG: a banner with Hebrew in it, on surfaces that draw Svg.
  • A button that talks to Claude: "לשלוח ל-Claude" sends Claude a short message and logs how many milliseconds it waited to enter the conversation; "לתיבת ההודעה" only puts it in the prompt box.
claude plugin install surface-probe@bar-mods

Then /surface-probe.

What it does outside the pane

The send button submits one prompt to the conversation, surface-probe: a button press reached the conversation., which starts a turn and costs that turn's tokens. Nothing else: no files, processes, network or model calls of its own.

Two things it was built around, both seen on 2026-10-04:

  • One press can fire twice. A second press while the first is sending is ignored, and the log says so.
  • The pane opens without taking the keyboard. A pane that holds the keyboard can keep a queued prompt from entering the conversation when the running turn ends.

Footprint

From claude plugin validate:

  • hooks: session.start, command.run{command=surface-probe}, ui.render{component=Pane, requestId=surface-probe}
  • calls: $.clock.now, $.command.register, $.prompt.fill, $.prompt.submit, $.state.get, $.state.set, $.ui.close, $.ui.open, $.ui.resolve

Surfaces

Draws in the terminal and the desktop app's Code tab. In claude -p, the SDK and the VS Code chat panel its hooks run and nothing draws.

Tested on Claude Code 2.1.288.

Source 2 files
hooks/register.tsx 100 lines
1// surface-probe: open it on a surface before designing a big UI for it. It shows what the surface
2// reports (surface, placement, body width, viewport), which elements it draws, how it lays out
3// right-to-left text, an SVG where the surface has one, and whether a button can hand Claude a
4// message, with how long that message waited.
5import { atom, read, update } from 'claude-code'
6import type { EngineInterface, Register } from 'claude-code'
7
8const PANE = 'surface-probe'
9const MESSAGE = 'surface-probe: a button press reached the conversation. (הלחיצה הגיעה לשיחה.)'
10
11const log = atom({ plugin: 'surface-probe', key: 'log' } as const, [] as string[])
12
13const SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="360" height="84" viewBox="0 0 360 84">
14  <rect width="360" height="84" rx="12" fill="#4c8dff"/>
15  <text x="340" y="36" direction="rtl" font-family="Arial, sans-serif" font-size="22" font-weight="700" fill="#ffffff">שלום מה-SVG</text>
16  <text x="340" y="64" direction="rtl" font-family="Arial, sans-serif" font-size="14" fill="#ffffff">3 פריטים · 1 באיחור · v2.1</text>
17</svg>`
18
19// One press can fire twice (seen 2026-10-04: two copies 0.4 s apart). This flag is set before the
20// first await, so the second press sees it; a flag in $.state would be read by both first.
21let isSending = false
22
23async function note($: EngineInterface, line: string): Promise<void> {
24  await update($, log, l => [...l, line].slice(-6))
25}
26
27async function send($: EngineInterface): Promise<void> {
28  if (isSending) {
29    await note($, 'second press ignored while the first is sending')
30    return
31  }
32  isSending = true
33  try {
34    const pressed = await $.clock.now()
35    await note($, 'pressed: sending')
36    await $.prompt.submit({ text: MESSAGE })
37    await note($, `entered the conversation after ${(await $.clock.now()) - pressed} ms`)
38  } finally {
39    isSending = false
40  }
41}
42
43async function fill($: EngineInterface): Promise<void> {
44  await $.prompt.fill({ text: MESSAGE })
45  await note($, 'put in the prompt box: press Enter to send')
46}
47
48// Opened without `focus`: a pane that holds the keyboard can keep a queued prompt from going in
49// when the running turn ends (seen 2026-10-04).
50async function open($: EngineInterface): Promise<string> {
51  const opened = await $.ui.open({ id: PANE, title: 'surface-probe', columns: 200 })
52  return opened.isPlaced ? 'surface-probe is open.' : `surface-probe did not open: ${opened.reason}`
53}
54
55export const register: Register = on => {
56  on('session.start', async ($, e, next) => {
57    await $.command.register({ name: 'surface-probe', description: 'Open a pane that shows what this surface draws and reports' })
58    return next(e)
59  })
60
61  on('command.run', { command: 'surface-probe' }, async $ => ({ text: await open($) }))
62
63  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
64    const el = $.ui.resolve(e)
65    const { Box, Text, Button } = el
66    const lines = await read($, log)
67    const v = e.viewport
68    const facts = `surface ${e.surface} · placement ${e.props.placement} · bodyColumns ${e.props.bodyColumns} · viewport ${v ? `${v.columns}x${v.rows}` : 'not measured'}${v?.isFullscreen === undefined ? '' : ` · isFullscreen ${v.isFullscreen}`}`
69    return (
70      <Box flexDirection="column" gap={1}>
71        <Text bold>surface-probe</Text>
72        <Text>{facts}</Text>
73        <Text>שלום, זו שורה בעברית בלבד.</Text>
74        <Text>{'Mixed: המשימה t03 נפתחה ב-4.10.2026 (Claude)'}</Text>
75        <Box flexDirection="row-reverse" gap={1} flexWrap="wrap">
76          <Text>שורה מימין לשמאל:</Text>
77          <Button key="send" label="לשלוח ל-Claude" variant="primary" onPress={() => send($).catch(err => note($, `send failed: ${String(err)}`))} />
78          <Button key="fill" label="לתיבת ההודעה" onPress={() => fill($).catch(err => note($, `fill failed: ${String(err)}`))} />
79          <Button key="clear" label="לנקות" dimColor onPress={() => update($, log, () => [])} />
80          <Button key="close" label="לסגור" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
81        </Box>
82        <Box flexDirection="column" alignItems="flex-end" borderStyle="round" paddingX={1}>
83          <Text>מיושר לימין</Text>
84          <Text dimColor>שורה שנייה, ארוכה יותר מהראשונה</Text>
85        </Box>
86        {lines.length > 0 && (
87          <Box flexDirection="column">
88            {lines.map((line, i) => <Text key={`log:${i}`} dimColor>{line}</Text>)}
89          </Box>
90        )}
91        {/* Branch on the surface, not on the table: resolve()'s table names Svg on the terminal too
92            (seen 2026-10-04), and a terminal pane that returns an Svg draws nothing. */}
93        {e.surface !== 'terminal' && 'Svg' in el
94          ? <el.Svg source={SVG} alt="An SVG with Hebrew text in it" width={360} height={84} />
95          : <Text dimColor>{`no Svg on ${e.surface}`}</Text>}
96      </Box>
97    )
98  })
99}
100
types/index.d.ts 8 lines
1export type ProbeLog = string[]
2
3declare module 'claude-code' {
4  interface PluginState {
5    'surface-probe': { log: ProbeLog }
6  }
7}
8