Draws Hebrew/Arabic messages, yours and Claude's, right-aligned with RTL direction marks.

Small mods for Claude Code, made for working in Hebrew.
| Mod | What it does |
|---|---|
rtl-messages | Hebrew and Arabic messages, yours and Claude's, drawn right to left in the terminal. The desktop app already does this by itself, so there the mod stays out of the way. |
usage-band | A band above the prompt: how much is left of the 5-hour and weekly limits, context fill and session cost. /usage-band hides or shows it. |
long-task-alert | A chime and a short message when a task took 5 minutes or more. The minutes are a setting. |
In a terminal running Claude Code, install the ones you want:
/plugin install rtl-messages --marketplace 3floors/claude-mods
/plugin install usage-band --marketplace 3floors/claude-mods
/plugin install long-task-alert --marketplace 3floors/claude-mods
In the desktop app: click + next to the prompt box, then Plugins, Add marketplace with 3floors/claude-mods, then Add plugin.
The chime in long-task-alert is an original sound made for this repository, free to use.
hooks/register.tsx 35 lines1import type { Register } from 'claude-code'
2
3import { isRtl, mark } from './rtl'
4
5// Only the terminal needs help: the desktop app already lays Hebrew out right to left,
6// and a tree drawn there by the mod would replace that with left-aligned rows.
7export const register: Register = on => {
8 on('ui.render', { component: 'UserMessage' }, ($, e, next) => {
9 if (e.surface !== 'terminal' || e.props.isExpanded || !isRtl(e.props.text)) return next(e)
10
11 const { Box, Text } = $.ui.resolve(e)
12
13 return (
14 <Box width="100%" flexDirection="column" alignItems="flex-end">
15 {mark(e.props.text)
16 .split('\n')
17 .map((line, i) => (
18 <Text key={String(i)}>{line || ' '}</Text>
19 ))}
20 </Box>
21 )
22 })
23
24 on('ui.render', { component: 'AssistantMessage' }, ($, e, next) => {
25 if (e.surface !== 'terminal' || !isRtl(e.props.text)) return next(e)
26 const { Box, Markdown } = $.ui.resolve(e)
27
28 return (
29 <Box width="100%" flexDirection="column" alignItems="flex-end">
30 <Markdown text={mark(e.props.text)} />
31 </Box>
32 )
33 })
34}
35hooks/rtl.ts 52 lines1// RLM only fixes one character; an isolate gives the whole line a right-to-left base,
2// so mixed Hebrew/English runs order correctly.
3const RLI = '\u2067'
4const PDI = '\u2069'
5const HE = /[-ࣿ]+/g
6const LAT = /[A-Za-z]+/g
7// list / quote / heading marker: the isolate opens after it so markdown still parses
8const BLOCK = /^(\s*(?:>\s*)*(?:(?:[-*+]|\d+[.)]|#{1,6})\s+)?)/
9
10const prose = (s: string) =>
11 s.replace(/```[\s\S]*?(```|$)/g, '').replace(/`[^`\n]*`/g, '').replace(/https?:\/\/\S+/g, '')
12
13const count = (s: string, re: RegExp) => (s.match(re) ?? []).length
14
15// RTL if Hebrew/Arabic words match or outnumber Latin ones, code and URLs not counted.
16export const isRtl = (s: string): boolean => {
17 const p = prose(s)
18 const he = count(p, HE)
19 return he > 0 && he >= count(p, LAT)
20}
21
22// Each Hebrew line is wrapped in RLI..PDI after its markdown marker; code fences, tables, blank and English lines are left alone.
23export const mark = (s: string): string => {
24 let inFence = false
25 return s
26 .split('\n')
27 .map(l => {
28 if (l.trimStart().startsWith('```')) inFence = !inFence
29 if (inFence || /^\s*\|/.test(l) || count(prose(l), HE) === 0) return l
30 const pre = BLOCK.exec(l)?.[1] ?? ''
31 const body = l.slice(pre.length).trimEnd()
32 return pre + RLI + body + PDI + l.slice(pre.length + body.length)
33 })
34 .join('\n')
35}
36
37// Paragraphs split on blank lines, except inside code fences.
38export const blocks = (s: string): string[] => {
39 const out: string[] = []
40 let cur: string[] = []
41 let inFence = false
42 for (const l of s.split('\n')) {
43 if (l.trimStart().startsWith('```')) inFence = !inFence
44 if (!inFence && !l.trim()) {
45 if (cur.length) out.push(cur.join('\n'))
46 cur = []
47 } else cur.push(l)
48 }
49 if (cur.length) out.push(cur.join('\n'))
50 return out
51}
52