SLOPSHOPPER

agent-dialogue

Claude Code skill that builds, debugs, audits and packages Claude Code mods from a plain-language description

newpanecommandtimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agent-dialogue
│ ┃ agent-dialogue ✕ › fix the failing auth test and add an audit log call │ ┃ ◆ Agent dialogue 0 messages │ ┃ ⏺ Read(src/auth.ts) │ ┃ Set the dialogue file in /config → ⎿ Read 6 lines │ ┃ agent-dialogue. ⏺ Update(src/auth.ts) │ ┃ ⎿ Added 2 lines, removed 1 line │ ┃ refreshes every 2 s · /dialogue · /agents ⏺ Bash(bun test) │ ┃ for status ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /dialogue │ ⎿ agent-dialogue: Set the dialogue file in /config → agent-dialogu │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · agent-dialogue
◆ Agent dialogue 0 messages Set the dialogue file in /config → agent-dialogue. refreshes every 2 s · /dialogue · /agents for status
README

agent-dialogue

Leia em português

A panel that shows, live, the conversation between helper agents that write to a shared file.

What is a mod?

Claude Code is Anthropic's assistant for working with code, in the terminal or in the Claude desktop app. A mod is a small add-on for it: it can add a panel, a bar above the box where you type, a new /command, or a rule that checks what Claude is about to do. This folder is one complete mod you can install and use as it is, or copy as a starting point.

What it does

A useful way to review work is to have two helper agents talk: a critic that finds problems and a builder that fixes them. They can leave messages for each other in a shared file using the small script scripts/say.mjs that comes with the skill. This mod shows that conversation as it grows, with a color per author and a filter, so you can follow the review without opening the file.

What you see

  • A panel titled "Agent dialogue" with the messages in order: who wrote, to whom, what kind of message (finding, fix, question, answer, verified, reopened, note) and the text.
  • A Show selector to see everyone or only one author.
  • If the file is not set yet, a line explaining how to set it.

How to use it

  1. In the settings, set file to the path of the conversation file (for example review.jsonl in a folder of your choice).
  2. Ask Claude to run a review with a critic and a builder that post with scripts/say.mjs to that file. Each post looks like node say.mjs --file review.jsonl critic builder finding "The login has no rate limit".
  3. Type /dialogue and press Enter to watch. It updates every 2 seconds.
  4. Use Show to filter by author; press Esc to go back to the prompt.

Keyboard shortcuts

KeyWhat it does
TabReach the Show selector
Arrow keys and EnterPick an author in Show
EscLeave the panel

Commands

TypeWhat it does
/dialogueOpen the conversation

Settings

Claude Code asks for these when you enable the mod. To change them later, open /config and find the mod by name.

SettingWhat it meansDefault
file (Dialogue file)Path of the conversation file the agents write to.(empty)
language (Language)Language of the text the mod shows: en (English) or pt-BR (Brazilian Portuguese).en

Good to know

  • Lines in the file that are broken or incomplete are skipped, not shown as errors.
  • There is no default file on purpose: you choose where the conversation lives.
  • Use it with agent-board (/agents) to see who is working while you read what they say.

Install

You need Claude Code 2.1.287 or newer in the terminal, or 2.1.286 or newer in the desktop app.

  1. Copy this folder anywhere on your computer.
  2. Inside the copy, rename the folder _claude-plugin to .claude-plugin (with a dot at the start). It ships without the dot so the examples never load by accident.
  3. Start Claude Code with the mod:
claude --plugin-dir path/to/agent-dialogue

To keep it installed for good, ask the claude-mod-forge skill to package it ("package this mod").

Check it

From the skill folder (skills/claude-mod-forge), this runs the manifest check, a strict type check and the 5 automated tests of this mod:

node scripts/check-mod.mjs examples/agent-dialogue

The tests cover the mod's logic and what it draws in the terminal and the desktop app. How it looks in your own window is worth a quick look in a real session.

For developers: the examples table lists the hooks and API calls this mod demonstrates.

Source 4 files
hooks/register.tsx 104 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { strings } from './i18n'
5import type { Strings } from './i18n'
6import { hhmm, parseDialogue } from './parse'
7
8// Shows a conversation that agents append to a JSONL file (scripts/say.mjs writes one).
9// Reads only: the file path comes from userConfig, never from the file's own content, and the
10// messages are drawn as text, never acted on.
11const PANE = 'agent-dialogue'
12const lines = atom({ plugin: 'agent-dialogue', key: 'lines' } as const, [])
13const filter = atom({ plugin: 'agent-dialogue', key: 'filter' } as const, 'all')
14const problem = atom({ plugin: 'agent-dialogue', key: 'problem' } as const, '')
15
16const COLORS = ['claude', 'suggestion', 'success', 'warning']
17
18async function poll($: EngineInterface, file: string, t: Strings) {
19  try {
20    if (!(await $.fs.exists(file))) {
21      await update($, problem, () => t.waiting)
22      return
23    }
24    const parsed = parseDialogue(await $.fs.read(file))
25    await update($, lines, () => parsed.slice(-200))
26    await update($, problem, () => '')
27  } catch {
28    await update($, problem, () => t.cannotRead)
29  }
30}
31
32export const register: Register = (on, options) => {
33  const file = String(options.file ?? '').trim()
34  const t = strings(options.language)
35
36  on('session.start', async ($, e, next) => {
37    if (file === '') {
38      // No default path: the pane says how to set one.
39      await update($, problem, () => t.noFile)
40    } else {
41      await poll($, file, t)
42      $.clock.every(2000, () => poll($, file, t))
43    }
44    try {
45      await $.command.register({ name: 'dialogue', description: t.commandDescription, immediate: true })
46    } catch {
47      // name taken
48    }
49    return next(e)
50  })
51
52  on('command.run', { command: 'dialogue' }, async $ => {
53    if (file === '') return { text: t.noFile }
54    await poll($, file, t)
55    const opened = await $.ui.open({ id: PANE, title: t.paneTitle, focus: true })
56    if (opened.isPlaced) return {}
57    // Nothing draws here (VS Code chat, claude -p): the last messages as text.
58    const all = await read($, lines)
59    return { text: all.length === 0 ? t.noMessages : all.slice(-15).map(l => `${l.from} → ${l.to || t.everyone}: ${l.text}`).join('\n') }
60  })
61
62  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
63    const ui = $.ui.resolve(e)
64    const { Box, Text } = ui
65    const all = await read($, lines)
66    const current = await read($, filter)
67    const note = file === '' ? t.noFile : await read($, problem)
68    const authors = [...new Set(all.map(l => l.from))]
69    const shown = all.filter(l => current === 'all' || l.from === current).slice(-Math.max(6, e.props.scroll.bodyRows - 6))
70    const color = (who: string) => COLORS[Math.max(0, authors.indexOf(who)) % COLORS.length] ?? 'text'
71
72    return (
73      <Box flexDirection="column" paddingX={1} gap={1}>
74        <Box justifyContent="space-between">
75          <Text bold color="claude">{t.heading}</Text>
76          <Text dimColor>{t.messages(all.length)}</Text>
77        </Box>
78        {e.surface !== 'mobile' && 'Select' in ui && authors.length > 0 && (
79          <ui.Select
80            key="author"
81            label={t.show}
82            value={current}
83            options={[{ value: 'all', label: t.everyone }, ...authors.map(a => ({ value: a, label: a }))]}
84            onSelect={value => update($, filter, () => value)}
85          />
86        )}
87        <Box flexDirection="column" gap={1}>
88          {shown.length === 0 && <Text dimColor>{note || t.noMessages}</Text>}
89          {shown.map((l, i) => (
90            <Box key={`msg-${i}-${l.ts}`} flexDirection="column">
91              <Box gap={1}>
92                <Text bold color={color(l.from)}>{l.from}</Text>
93                <Text dimColor>{`→ ${l.to || t.everyone} · ${t.kind[l.kind] ?? l.kind}${hhmm(l.ts) ? ` · ${hhmm(l.ts)}` : ''}`}</Text>
94              </Box>
95              <Text wrap="wrap">{l.text}</Text>
96            </Box>
97          ))}
98        </Box>
99        <Text dimColor>{t.footer}</Text>
100      </Box>
101    )
102  })
103}
104
hooks/i18n.ts 58 lines
1// The text this mod shows, per language. Pure: no $ calls, so tests can cover it directly.
2// Anything other than a known language falls back to English.
3
4export type Language = 'en' | 'pt-BR'
5
6const en = {
7  kind: {
8    finding: 'finding',
9    fix: 'fix',
10    question: 'question',
11    answer: 'answer',
12    verified: 'verified',
13    reopen: 'reopened',
14    note: 'note',
15  } as Record<string, string>,
16  waiting: 'waiting for the agents to start writing…',
17  cannotRead: 'could not read the dialogue file',
18  noFile: 'Set the dialogue file in /config → agent-dialogue.',
19  commandDescription: 'Show the conversation between the agents',
20  paneTitle: 'Agent dialogue',
21  heading: '◆ Agent dialogue',
22  messages: (n: number) => `${n} messages`,
23  show: 'Show',
24  everyone: 'everyone',
25  noMessages: 'No messages yet.',
26  footer: 'refreshes every 2 s · /dialogue · /agents for status',
27}
28
29const ptBR: typeof en = {
30  kind: {
31    finding: 'achado',
32    fix: 'correção',
33    question: 'pergunta',
34    answer: 'resposta',
35    verified: 'conferido',
36    reopen: 'reaberto',
37    note: 'nota',
38  },
39  waiting: 'aguardando os agentes começarem a escrever…',
40  cannotRead: 'não consegui ler o arquivo do diálogo',
41  noFile: 'Defina o arquivo do diálogo em /config → agent-dialogue.',
42  commandDescription: 'Mostra a conversa entre os agentes',
43  paneTitle: 'Diálogo dos agentes',
44  heading: '◆ Diálogo dos agentes',
45  messages: (n: number) => `${n} mensagens`,
46  show: 'Mostrar',
47  everyone: 'todos',
48  noMessages: 'Nenhuma mensagem ainda.',
49  footer: 'atualiza a cada 2 s · /dialogue · /agents para o status',
50}
51
52export type Strings = typeof en
53const TABLE: Record<Language, Strings> = { en, 'pt-BR': ptBR }
54
55export function strings(language: unknown): Strings {
56  return language === 'pt-BR' ? TABLE['pt-BR'] : TABLE.en
57}
58
hooks/parse.ts 30 lines
1import type { Line } from '../types'
2
3// Tolerant JSONL parser: a half-written last line (an agent mid-append) is skipped, not fatal.
4export function parseDialogue(text: string): Line[] {
5  const out: Line[] = []
6  for (const raw of text.split('\n')) {
7    const line = raw.trim()
8    if (line === '') continue
9    try {
10      const v = JSON.parse(line) as Record<string, unknown>
11      if (typeof v.from !== 'string' || typeof v.text !== 'string') continue
12      out.push({
13        from: v.from.slice(0, 20),
14        to: typeof v.to === 'string' ? v.to.slice(0, 20) : '',
15        kind: typeof v.kind === 'string' ? v.kind.slice(0, 20) : 'note',
16        text: v.text.slice(0, 2000),
17        ts: typeof v.ts === 'string' ? v.ts : '',
18      })
19    } catch {
20      // skip
21    }
22  }
23  return out
24}
25
26export function hhmm(ts: string): string {
27  const d = new Date(ts)
28  return Number.isNaN(d.getTime()) ? '' : `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
29}
30
types/index.d.ts 8 lines
1export type Line = { from: string; to: string; kind: string; text: string; ts: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'agent-dialogue': { lines: Line[]; filter: string; problem: string }
6  }
7}
8