SLOPSHOPPER

vurgu

Araçları tek satıra katlar, mesajları seçilen renkle vurgular

newpanerowsguardcommandtoast
v0.1.0MITupdated 2026-10-05yasinozmeen/claude-code-mods/vurgu
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · vurgu
│ ┃ Vurgu renkleri ✕ ❯ fix the failing auth test and add an audit log call │ ┃ Senin mesajların │ ┃ ❯ Örnek mesaj böyle görünür ⏺ Read(src/auth.ts) │ ┃ [yeşil] deniz mavi lacivert mor ⎿ Read 6 lines │ ┃ Başka renk: #14532d ⏎ Uygula ⏺ Bash(bun test) │ ┃ ⎿ 3 pass, 1 fail │ ┃ Claude: kapanış mesajı │ ┃ ⏺ Örnek mesaj böyle görünür Done. I made refresh reject expired claims and added an audit call │ ┃ yeşil deniz [mavi] lacivert mor │ ┃ Başka renk: #1e3a5f ⏎ Uygula ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ Claude: iş arası mesajı › /vurgu │ ┃ ⏺ Örnek mesaj böyle görünür ⎿ vurgu: Renk paneli açıldı. │ ┃ yeşil deniz mavi lacivert [mor] │ ┃ Başka renk: #3f2d63 ⏎ Uygula │ ┃ │ ┃ Seçim hemen uygulanır ve hatırlanır. │ ┃ Kapatmak için: /vurgu kapat │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Vurgu renkleri
Senin mesajların ❯ Örnek mesaj böyle görünür [yeşil] deniz mavi lacivert mor bordo kahve Başka renk: #14532d ⏎ Uygula Claude: kapanış mesajı ⏺ Örnek mesaj böyle görünür yeşil deniz [mavi] lacivert mor bordo kahve Başka renk: #1e3a5f ⏎ Uygula Claude: iş arası mesajı ⏺ Örnek mesaj böyle görünür yeşil deniz mavi lacivert [mor] bordo kahve Başka renk: #3f2d63 ⏎ Uygula Seçim hemen uygulanır ve hatırlanır. Kapatmak için: /vurgu kapat
Your message
❯ fix the failing auth test and add an audit log call
Claude's reply
Done. I made refresh reject expired claims and added an audit call.
README

Vurgu

Claude Code modu. Uzun bir çalışmada asıl okunacak şeyi, yani yazıları öne çıkarır.

Katlanmış araçlar, renkli mesajlar ve sağda renk paneli

  • Araçları katlar. Arka arkaya çalışan araçlar tek soluk satır olur: ▸ 4 işlem · Bash ×3, Edit. Satıra tıklayınca açılır; açıkken her aracın üstündeki satıra tıklamak yeniden katlar. Çalışmakta olan ve hata veren araç gizlenmez. Araya yazı girince grup orada bölünür, yazı hiçbir zaman gizlenmez. Claude'un sana gönderdiği dosyalar da gizlenmez.
  • Mesajları renklendirir. Üç tür ayrı renk alır: senin mesajların, Claude'un iş arasında yazdıkları (ardından araç çalıştırdığı yazılar) ve kapanış mesajı.

Renk seçimi

/vurgu sağda renk panelini açar, /vurgu kapat kapatır. Her tür için hazır tonlardan biri, renksiz ya da #rrggbb biçiminde kendi rengin seçilir. Seçim hemen uygulanır ve sonraki oturumlarda hatırlanır.

Kurulum

Gerekenler: Claude Code 2.1.287+, bir terminal (mod yalnızca terminalde çizer), /usr/bin/python3.

Depoyu indirip klasörü ~/.claude/settings.json içindeki env bölümüne ekle (ana README):

"CLAUDE_CODE_PLUGIN_DIRS": "~/claude-code-mods/vurgu"

Bilinen sınırlar

  • Terminal, yukarıda kalan satırları yeniden çizmez: mod yüklenmeden önce çizilmiş mesajlar eski haliyle kalır, oturum yeniden açılınca düzelir.
  • Renkli mesajları mod kendisi çizer; motorun kendi satırı renkli bir kutunun içine konamıyor.
  • Açılmış bir aracın çıktısına tıklamak grubu katlamaz; yalnızca araçların üstündeki satırlar tıklanır.
  • Yeni bir yazı önce kapanış renginde çıkar, ardından araç çalışırsa iş arası rengine döner.

Geliştirme

claude plugin validate .
claude plugin test .
Dosyaİçerik
hooks/register.tsxModun kendisi: katlama, renkler, panel
bin/sira.pyOturum kaydından mesaj ve araç sırasını çıkarır
types/index.d.tsModun tuttuğu değerlerin tipleri
Source 2 files
hooks/register.tsx 386 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Colors, Run } from '../types'
5
6type Who = 'ben' | 'claude'
7type Row = { id: string; who: Who | 'arac'; isMid: boolean; tool: string }
8
9const PANE = 'vurgu'
10const SAID = ['SendUserFile']
11const KINDS = [
12  { key: 'ben', label: 'Senin mesajların' },
13  { key: 'son', label: 'Claude: kapanış mesajı' },
14  { key: 'ara', label: 'Claude: iş arası mesajı' },
15] as const
16const PALETTE = [
17  { name: 'yeşil', value: '#14532d' },
18  { name: 'deniz', value: '#134e4a' },
19  { name: 'mavi', value: '#1e3a5f' },
20  { name: 'lacivert', value: '#1e1b4b' },
21  { name: 'mor', value: '#3f2d63' },
22  { name: 'bordo', value: '#4c1d2e' },
23  { name: 'kahve', value: '#4a3410' },
24  { name: 'zeytin', value: '#3a3f17' },
25  { name: 'gri', value: '#2f3338' },
26] as const
27const DEFAULTS: Colors = { ben: '#14532d', son: '#1e3a5f', ara: '#3f2d63' }
28// The background of each kind of message, chosen in the pane.
29const colors = atom({ plugin: 'vurgu', key: 'colors' } as const, DEFAULTS)
30// The reply texts written mid-work, a tool call after them.
31const ara = atom({ plugin: 'vurgu', key: 'ara' } as const, [] as string[])
32// The reply text drawn last in this turn; a tool call after it makes it mid-work.
33const latest = { id: '' }
34// The reply texts already drawn once.
35const seen = new Set<string>()
36// The runs of tool calls, each drawn as one line, and the runs opened out.
37const runs = atom({ plugin: 'vurgu', key: 'runs' } as const, [] as Run[])
38const open = atom({ plugin: 'vurgu', key: 'open' } as const, [] as string[])
39// True once something other than a tool call was drawn: the next call starts a run.
40const live = { isBroken: true }
41// The session's prompts, reply texts and tool calls in transcript order,
42// read from its stored transcript: what was written before the mod loaded.
43async function ordered($: EngineInterface): Promise<Row[]> {
44  try {
45    const argv = ['/usr/bin/python3', `${$.plugin.root}/bin/sira.py`, await $.session.id()]
46    const out = await $.process.run(argv, { timeoutMs: 15_000 })
47
48    return out.stdout
49      .split('\n')
50      .filter(line => line.length > 2)
51      .map(line => {
52        const [mark = '', id = '', tool = ''] = line.split(' ')
53        const who = mark === 'b' ? ('ben' as const) : mark === 't' ? ('arac' as const) : ('claude' as const)
54        // A file sent to the person (`s`) counts as something Claude said.
55
56        return { id, who, isMid: mark === 'a', tool }
57      })
58  } catch {
59    return []
60  }
61}
62
63// The tool calls of the transcript gathered into runs: calls with no prompt
64// and no reply text between them.
65function gathered(rows: Row[]): Run[] {
66  const made: Run[] = []
67  let isOpen = false
68
69  for (const row of rows) {
70    if (row.who !== 'arac') {
71      isOpen = false
72    } else if (isOpen) {
73      made.at(-1)?.ids.push(row.id)
74      made.at(-1)?.tools.push(row.tool)
75    } else {
76      made.push({ ids: [row.id], tools: [row.tool] })
77      isOpen = true
78    }
79  }
80
81  return made
82}
83
84// What a run says of itself in one line: how many calls, of which tools.
85function summed(run: Run): string {
86  const counts = new Map<string, number>()
87
88  for (const tool of run.tools) {
89    counts.set(tool, (counts.get(tool) ?? 0) + 1)
90  }
91
92  const parts = [...counts].map(([tool, count]) => (count > 1 ? `${tool} ×${count}` : tool))
93
94  return `${run.ids.length} işlem · ${parts.join(', ')}`
95}
96
97// Sets one kind's colour, kept across sessions; anything but `#rrggbb` or ''
98// is not a colour and changes nothing.
99async function paint($: EngineInterface, kind: keyof Colors, value: string) {
100  const spelled = value.trim().toLowerCase()
101  const hex = spelled === '' || spelled.startsWith('#') ? spelled : `#${spelled}`
102
103  if (hex !== '' && !/^#[0-9a-f]{6}$/.test(hex)) {
104    $.ui.toast('Renk #rrggbb biçiminde olmalı, örneğin #14532d')
105
106    return
107  }
108
109  await update($, colors, now => ({ ...now, [kind]: hex }))
110  await $.store.set('colors', await read($, colors))
111}
112
113// What a tool call is about, in a few words: its own description, or the
114// first line of what it was given.
115function about(tool: string, input: unknown): string {
116  const given = typeof input === 'object' && input !== null ? (input as Record<string, unknown>) : {}
117  const said = [given.description, given.command, given.file_path, given.pattern, given.prompt, given.url]
118  const first = said.find(one => typeof one === 'string' && one.trim() !== '')
119  const line = typeof first === 'string' ? (first.trim().split('\n')[0] ?? '') : ''
120
121  return line === '' ? tool : `${tool} · ${line.length > 90 ? `${line.slice(0, 90)}…` : line}`
122}
123
124// A prompt as the person reads it: the tags the engine wraps pasted text in
125// are its own bookkeeping, and so are the blank lines they leave.
126function shownText(text: string): string {
127  return text
128    .replace(/<\/?pasted_content[^>]*>[ \t]*\n?/g, '')
129    .replace(/\n{3,}/g, '\n\n')
130    .trim()
131}
132
133async function toggle($: EngineInterface, head: string) {
134  await update($, open, list => (list.includes(head) ? list.filter(one => one !== head) : [...list, head].slice(-200)))
135}
136
137export const register: Register = on => {
138  // The runs made before the mod loaded, from the transcript.
139  on('session.start', async ($, e, next) => {
140    const started = await next(e)
141    await $.command.register({ name: 'vurgu', description: 'Mesaj renklerini seçme panelini aç; /vurgu kapat kapatır' })
142    const kept = await $.store.get('colors')
143
144    if (typeof kept === 'object' && kept !== null) {
145      await update($, colors, () => ({ ...DEFAULTS, ...(kept as Partial<Colors>) }))
146    }
147
148    const rows = await ordered($)
149    await update($, runs, () => gathered(rows).slice(-400))
150    await update($, ara, () => rows.filter(row => row.isMid).map(row => row.id).slice(-600))
151
152    return started
153  })
154
155  on('command.run', { command: 'vurgu' }, async ($, e) => {
156    if (e.args.trim() === 'kapat') {
157      await $.ui.close({ id: PANE })
158
159      return { text: 'Renk paneli kapatıldı.' }
160    }
161
162    await $.ui.open({ id: PANE, title: 'Vurgu renkleri', columns: 50 })
163
164    return { text: 'Renk paneli açıldı.' }
165  })
166
167  // The colours, each kind with the palette, a field for any other colour
168  // and a sample line: a choice shows there and in the chat at once.
169  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
170    if (e.surface !== 'terminal') {
171      const { Text } = $.ui.resolve(e)
172
173      return <Text dimColor>Renk paneli yalnızca terminalde çizilir.</Text>
174    }
175
176    const { Box, Button, Input, Text } = $.ui.resolve(e)
177    const now = await read($, colors)
178
179    return (
180      <Box flexDirection="column" rowGap={1}>
181        {KINDS.map(kind => (
182          <Box flexDirection="column">
183            <Text bold>{kind.label}</Text>
184            <Box flexDirection="row" width="100%" paddingX={1} {...(now[kind.key] === '' ? {} : { backgroundColor: now[kind.key] })}>
185              <Text>{kind.key === 'ben' ? '❯ Örnek mesaj böyle görünür' : '⏺ Örnek mesaj böyle görünür'}</Text>
186            </Box>
187            <Box flexDirection="row" flexWrap="wrap" columnGap={1}>
188              {PALETTE.map(tone => (
189                <Box flexDirection="row">
190                  <Text backgroundColor={tone.value}>{'  '}</Text>
191                  <Button
192                    key={`${kind.key}-${tone.name}`}
193                    label={now[kind.key] === tone.value ? `[${tone.name}]` : tone.name}
194                    plain
195                    onPress={() => paint($, kind.key, tone.value)}
196                  />
197                </Box>
198              ))}
199              <Button
200                key={`${kind.key}-yok`}
201                label={now[kind.key] === '' ? '[renksiz]' : 'renksiz'}
202                plain
203                dimColor
204                onPress={() => paint($, kind.key, '')}
205              />
206            </Box>
207            <Input
208              key={`${kind.key}-hex`}
209              label="Başka renk"
210              placeholder={now[kind.key] === '' ? '#rrggbb' : now[kind.key]}
211              submitLabel="Uygula"
212              onSubmit={value => paint($, kind.key, value)}
213            />
214          </Box>
215        ))}
216        <Text dimColor>Seçim hemen uygulanır ve hatırlanır. Kapatmak için: /vurgu kapat</Text>
217      </Box>
218    )
219  })
220
221  on('prompt.submit', ($, e, next) => {
222    live.isBroken = true
223    latest.id = ''
224
225    return next(e)
226  })
227
228  on('tool.call', async ($, e, next) => {
229    const mid = latest.id
230
231    if (mid !== '' && !(await read($, ara)).includes(mid)) {
232      await update($, ara, list => [...list, mid].slice(-600))
233    }
234
235    // A file sent to the person is something said, not work: it is never
236    // folded, and the calls after it start a run of their own.
237    if (SAID.includes(e.tool)) {
238      live.isBroken = true
239
240      return next(e)
241    }
242
243    // The call joins the run before it, or starts one after a reply's text.
244    const startsRun = live.isBroken
245    live.isBroken = false
246    await update($, runs, list => {
247      const last = list.at(-1)
248
249      if (list.some(run => run.ids.includes(e.tool_use_id))) {
250        return list
251      }
252
253      if (startsRun || last === undefined) {
254        return [...list, { ids: [e.tool_use_id], tools: [e.tool] }].slice(-400)
255      }
256
257      return [...list.slice(0, -1), { ids: [...last.ids, e.tool_use_id], tools: [...last.tools, e.tool] }]
258    })
259
260    return next(e)
261  })
262
263  // A run of tool calls is one dim line, pressed to open out and to fold
264  // again; a call still running, or one that failed, shows as the engine
265  // draws it.
266  on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
267    const run = (await read($, runs)).find(one => one.ids.includes(e.requestId))
268
269    if (e.surface !== 'terminal' || run === undefined) {
270      return next(e)
271    }
272
273    const head = run.ids[0] ?? ''
274    const isOpen = (await read($, open)).includes(head)
275    const isShown = isOpen || e.props.isRunning || e.props.isErrored
276    const { Box, Button } = $.ui.resolve(e)
277
278    if (e.requestId !== head) {
279      if (!isShown) {
280        return <Box />
281      }
282
283      // Opened out, each call has a line of its own that folds the run again,
284      // so the one at hand does, not only the run's first far above.
285      return isOpen ? (
286        <Box flexDirection="column">
287          <Button key={`fold-${e.requestId}`} label={`▾ ${about(e.props.tool, e.props.input)}`} plain dimColor onPress={() => toggle($, head)} />
288          {await next(e)}
289        </Box>
290      ) : (
291        next(e)
292      )
293    }
294
295    const label = `${isOpen ? '▾' : '▸'} ${summed(run)}`
296
297    const fold = <Button key={`run-${head}`} label={label} plain dimColor onPress={() => toggle($, head)} />
298
299    if (!isShown) {
300      return <Box>{fold}</Box>
301    }
302
303    return (
304      <Box flexDirection="column">
305        {fold}
306        {await next(e)}
307      </Box>
308    )
309  })
310
311  on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
312    const run = (await read($, runs)).find(one => one.ids.includes(e.requestId))
313
314    if (e.surface !== 'terminal' || run === undefined) {
315      return next(e)
316    }
317
318    const isOpen = (await read($, open)).includes(run.ids[0] ?? '')
319    const { Box } = $.ui.resolve(e)
320
321    return isOpen || e.props.isErrored ? next(e) : <Box />
322  })
323
324  // A prompt and a reply's text are drawn on the colour chosen for their
325  // kind. The engine's own row may not be set inside a coloured box, so the
326  // mod draws the row; a kind left without a colour stays the engine's.
327  on('ui.render', { component: 'UserMessage' }, async ($, e, next) => {
328    const tone = (await read($, colors)).ben
329
330    if (e.surface !== 'terminal' || tone === '' || e.props.task !== undefined || e.props.from !== undefined) {
331      return next(e)
332    }
333
334    const { Box, Text } = $.ui.resolve(e)
335
336    return (
337      <Box flexDirection="row" width="100%" backgroundColor={tone} paddingX={1}>
338        <Text bold>❯ </Text>
339        <Box flexDirection="column" flexGrow={1} flexShrink={1}>
340          <Text>{shownText(e.props.text)}</Text>
341        </Box>
342      </Box>
343    )
344  })
345
346  on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
347
348    // A text seen for the first time is new in this turn: it ends the run of
349    // tool calls before it. A row drawn again (a colour changed, the window
350    // scrolled) says nothing new.
351    if (!seen.has(e.requestId)) {
352      seen.add(e.requestId)
353      live.isBroken = true
354      latest.id = e.requestId
355    }
356
357    const now = await read($, colors)
358    const tone = (await read($, ara)).includes(e.requestId) ? now.ara : now.son
359
360    if (e.surface !== 'terminal' || tone === '') {
361      return next(e)
362    }
363
364    const { Box, Markdown, Text } = $.ui.resolve(e)
365    const drawn = await next(e)
366    // Another mod's drawing (Vitrin's, with its pressable paths) is kept and
367    // set on the colour; the engine's own is drawn again here.
368    const isMods = typeof drawn === 'object' && drawn !== null && 'type' in drawn && drawn.type === 'Box'
369
370    return (
371      <Box flexDirection="row" width="100%" backgroundColor={tone} paddingX={1}>
372        {isMods ? (
373          drawn
374        ) : (
375          <Box flexDirection="row" flexGrow={1} flexShrink={1}>
376            <Text>{e.props.isFirstOfReply ? '⏺ ' : '  '}</Text>
377            <Box flexDirection="column" flexGrow={1} flexShrink={1}>
378              <Markdown text={e.props.text} />
379            </Box>
380          </Box>
381        )}
382      </Box>
383    )
384  })
385}
386
types/index.d.ts 14 lines
1// A run of tool calls with no text between them: drawn as one line.
2export type Run = { ids: string[]; tools: string[] }
3
4// The background of each kind of message, as `#rrggbb`; '' leaves the kind
5// as the engine draws it. `ben` is the person's prompt, `son` the text that
6// closes a reply, `ara` a text written mid-work.
7export type Colors = { ben: string; son: string; ara: string }
8
9declare module 'claude-code' {
10  interface PluginState {
11    vurgu: { runs: Run[]; open: string[]; ara: string[]; colors: Colors }
12  }
13}
14