SLOPSHOPPER

terminal-browser

/browse <url or file> opens a pane beside the chat showing a screenshot (where the terminal supports images) and the page text, so you can review pages without…

newpaneguardcommandprocessnetwork
v0.1.0no licenseupdated 2026-10-11KianaVega/KianaVega/mods/terminal-browser
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · terminal-browser
│ ┃ browser ✕ › fix the failing auth test and add an audit log call │ ┃ Nothing open. Try /browse │ ┃ https://example.com ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ 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 │ │ › /browse │ ⎿ terminal-browser: Usage: /browse <url or html file>. With no arg │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · browser
Nothing open. Try /browse https://example.com
README

Claude Code mods

Nine mods for Claude Code (2.1.287 or newer), written from the walkthrough video. Each lives in mods/<name>.

Install one, or all, from a terminal session:

/plugin install <name> --marketplace KianaVega/KianaVega
ModWhat it doesExtra model usage
savvy-progressProgress bar above the prompt and a live list of subagents with model, tokens and rough costnone
tool-skinsIcons and colors for tool calls in seven themes, switch with /skin <name>none
you-should-knowA second, small model reads long answers and flags anything important you might missone small call per long answer
file-treePane showing files Claude reads and edits, green once committed (/file-tree)none
cache-taxHolds a prompt that would reload a cold cache and shows an estimate. /keepwarm pings before the cache expiresnone, except keep-warm pings
blast-radiusStops recursive deletes, git clean, hard resets and shows the affected files before you press Proceednone
reflectOffers to save corrections as a rule in CLAUDE.mdone small call when you save
terminal-browser/browse <url or file> opens a pane with a screenshot (kitty or Ghostty) and the page textnone
replay-theaterReplay button after a turn that steps through each edit as a diff (/replay)none

Notes

  • Settings such as trigger phrases, price tables, skins and which model the watcher uses are constants at the top of each hooks/register.tsx. Edit and reload.
  • Cost figures are estimates from a built-in price table.
  • terminal-browser is a viewer built from a headless screenshot and page text. It is not a live, clickable browser.
  • Mods run in the terminal and the desktop app's Code tab, not the VS Code extension.
Source 2 files
hooks/register.tsx 131 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Page } from '../types'
5
6const PANE = 'browser'
7const SHOT_DIR = '/tmp/terminal-browser'
8const BROWSERS = ['chromium', 'chromium-browser', 'google-chrome', 'google-chrome-stable']
9const MAX_TEXT = 6000
10
11const page = atom({ plugin: 'terminal-browser', key: 'page' } as const, null as Page | null)
12
13const toText = (html: string): { title: string; text: string } => {
14  const title = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1]?.trim() ?? ''
15  const text = html
16    .replace(/<(script|style|noscript)[\s\S]*?<\/\1>/gi, '')
17    .replace(/<(h[1-6])[^>]*>([\s\S]*?)<\/\1>/gi, (_m, tag: string, inner: string) => `\n${'#'.repeat(Number(tag[1]))} ${inner}\n`)
18    .replace(/<li[^>]*>/gi, '\n- ')
19    .replace(/<(br|\/p|\/div|\/tr)[^>]*>/gi, '\n')
20    .replace(/<[^>]+>/g, '')
21    .replace(/&nbsp;/g, ' ')
22    .replace(/&amp;/g, '&')
23    .replace(/&lt;/g, '<')
24    .replace(/&gt;/g, '>')
25    .replace(/\n{3,}/g, '\n\n')
26    .trim()
27
28  return { title, text: text.slice(0, MAX_TEXT) }
29}
30
31export const register: Register = on => {
32  let lastHtml: string | undefined
33
34  on('session.start', async ($, e, next) => {
35    await $.command.register({
36      name: 'browse',
37      description: 'Open a URL or local HTML file in a pane beside the chat',
38      argumentHint: '<url | file>',
39    })
40
41    return next(e)
42  })
43
44  // Remember the last HTML file Claude wrote so a bare /browse can open it.
45  on('tool.call', { tool: 'Write' }, async ($, e, next) => {
46    if (e.file_path.endsWith('.html')) lastHtml = e.file_path
47
48    return next(e)
49  })
50
51  on('command.run', { command: 'browse' }, async ($, e) => {
52    const target = e.args.trim() || lastHtml
53    if (target === undefined || target === '') {
54      return { text: 'Usage: /browse <url or html file>. With no argument it opens the last HTML file Claude wrote.' }
55    }
56    const isUrl = /^https?:\/\//i.test(target)
57    const cwd = await $.session.cwd()
58    const abs = target.startsWith('/') ? target : `${cwd}/${target}`
59    const url = isUrl ? target : `file://${abs}`
60
61    let body = ''
62    let note = ''
63    try {
64      body = isUrl ? (await $.http.fetch(target)).text : String(await $.fs.read(abs))
65    } catch {
66      note = 'Could not load the page text.'
67    }
68    const { title, text } = toText(body)
69
70    let shot: string | null = null
71    try {
72      const found = await $.process.run(['sh', '-c', `command -v ${BROWSERS.join(' ')} | head -1`])
73      const browser = found.stdout.trim()
74      if (browser !== '') {
75        const out = `${SHOT_DIR}/shot-${await $.clock.now()}.png`
76        await $.fs.write(`${SHOT_DIR}/.keep`, '')
77        const ran = await $.process.run(
78          [browser, '--headless', '--no-sandbox', '--disable-gpu', `--screenshot=${out}`, '--window-size=1280,800', url],
79          { timeoutMs: 30000 },
80        )
81        if (ran.exitCode === 0) shot = out
82      }
83    } catch {
84      note = `${note} No screenshot (no headless browser available).`.trim()
85    }
86
87    await update($, page, () => ({ url, title, text, shot, note }))
88    await $.ui.open({ id: PANE, title: 'Browser' })
89
90    return { text: `Opened ${url} in the Browser pane.` }
91  })
92
93  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
94    const current = await read($, page)
95
96    if (current === null) {
97      const { Text } = $.ui.resolve(e)
98
99      return <Text dimColor>Nothing open. Try /browse https://example.com</Text>
100    }
101
102    // Only the terminal surface draws pictures; others get the text alone.
103    const picture =
104      e.surface === 'terminal' && current.shot !== null
105        ? (() => {
106            const { Image } = $.ui.resolve(e)
107
108            return (
109              <Image
110                source={{ file: current.shot, format: 'png' }}
111                columns={Math.max(20, e.props.bodyColumns)}
112                rows={14}
113                alt="Page screenshot"
114              />
115            )
116          })()
117        : null
118    const { Box, Text, Markdown } = $.ui.resolve(e)
119
120    return (
121      <Box flexDirection="column">
122        <Text bold>{current.title || current.url}</Text>
123        <Text dimColor>{current.url}</Text>
124        {current.note !== '' && <Text color="warning">{current.note}</Text>}
125        {picture}
126        <Markdown text={current.text} />
127      </Box>
128    )
129  })
130}
131
types/index.d.ts 8 lines
1export type Page = { url: string; title: string; text: string; shot: string | null; note: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'terminal-browser': { page: Page | null }
6  }
7}
8