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…

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.
row-reverse row and a right-aligned box. The terminal draws Hebrew in reverse; the desktop draws it right.Svg.claude plugin install surface-probe@bar-mods
Then /surface-probe.
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:
From claude plugin validate:
session.start, command.run{command=surface-probe}, ui.render{component=Pane, requestId=surface-probe}$.clock.now, $.command.register, $.prompt.fill, $.prompt.submit, $.state.get, $.state.set, $.ui.close, $.ui.open, $.ui.resolveDraws 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.
hooks/register.tsx 100 lines1// 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}
100types/index.d.ts 8 lines1export type ProbeLog = string[]
2
3declare module 'claude-code' {
4 interface PluginState {
5 'surface-probe': { log: ProbeLog }
6 }
7}
8