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

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

▸ 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./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.
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"
claude plugin validate .
claude plugin test .
| Dosya | İçerik |
|---|---|
hooks/register.tsx | Modun kendisi: katlama, renkler, panel |
bin/sira.py | Oturum kaydından mesaj ve araç sırasını çıkarır |
types/index.d.ts | Modun tuttuğu değerlerin tipleri |
hooks/register.tsx 386 lines1import { 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}
386types/index.d.ts 14 lines1// 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