SLOPSHOPPER

project-color

A colour per project: a full-width stripe in the repo's colour above the prompt (stacked over other bands), a colour-emoji status line for mobile, and /color…

newbandrowscommandstatus
v0.1.0MITupdated 2026-10-06mrjk05/modemon/mods/project-color
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · project-color
› fix the failing auth test and add an audit log call ⏺ 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 › /color ▌ app purple #8B5CF6 · auto ● purple blue teal green lime yellow amber orange red pink magenta slate /color <name|#hex> pins · /color auto resets ▌ app ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ project-color: 🟣 app

Draws

Band
▌ app
Command output
▌ app purple #8B5CF6 · auto ● purple blue teal green lime yellow amber orange red /color <name|#hex> pins · /color auto resets
README

project-color

A colour per project, so you can tell at a glance which repo a Claude Code window belongs to. A full-width stripe in the project's colour sits above the prompt with the project name on the left and the current branch, dimmed, on the right.

 ▌ modemon                                                        feature/x
╭──────────────────────────────────────────────────────────────────────────╮
│ >                                                                        │
╰──────────────────────────────────────────────────────────────────────────╯
  🟣 modemon

The stripe row is a solid colour band (purple here) and the name is bold in black or white, whichever has more contrast on it.

  • Project identity: the basename of the git repository root (the main working tree's root, for a worktree). Outside a repo it uses the session's project root, then the working directory.
  • Automatic colour: a stable 32-bit FNV-1a hash of the project name, mod 12, picks one of the palette's colours. The same repo gets the same colour in every session and on every machine.
  • Palette: purple, blue, teal, green, lime, yellow, amber, orange, red, pink, magenta, slate. They are mid-tones chosen to stand out on both dark and light terminal themes. Each gets black or white text from its WCAG luminance (always at least 4.5:1 contrast) and a colour emoji for the status line.
  • Shared band: the stripe is drawn on top, and any other band mod (context-bar, checklist, ...) is drawn beneath it in the same column. It steps aside while a survey holds the band.
  • Branch hint: read from .git/HEAD (worktree .git files are followed) at session start and after each turn. A detached HEAD shows a short commit. It is dropped when the band is too narrow, or skipped if it can't be read.

Install

/plugin install project-color --marketplace mrjk05/modemon

Answer y to add the marketplace, then pick a scope.

Commands

CommandWhat it does
/colorShows the current colour and the palette as swatches. Also works as /color list.
/color <name>Pins a palette colour for this repo, e.g. /color teal.
/color <#hex>Pins any colour: #RGB, #RRGGBB or a bare RRGGBB. Anything else is refused and the palette is listed.
/color autoRemoves the pin and goes back to the automatic colour. Also works as /color reset.
/project-color ...The same command under the mod's own name. It is always registered, in case a Claude Code build reserves /color for itself.

A pin is saved in the mod's $.store, keyed by the repo root, so it lasts across sessions and only applies to that repo.

The /color output is drawn as a small tree: the project in its colour, then the 12 swatches with the current one marked ●. Its plain-text form, which the model reads and which is shown wherever the tree can't be drawn, names the colour, its hex and whether it is auto or pinned.

Surfaces

SurfaceStripe above the prompt/color swatchesStatus line
TerminalYesYes🟣 modemon
Desktop (Code tab)YesYes🟣 modemon
Claude mobile appNo, the app does not raise the bandYes, inline in the transcript🟣 modemon: this is how the colour shows on the phone

Configuration

Set these in /config (or under pluginConfigs.project-color in settings):

OptionDefaultMeaning
stripetrueDraw the stripe above the prompt.
thickness"1""2" draws a two-row stripe. It falls back to one row when the band has fewer than 3 rows of room.
branchtrueShow the branch at the right end of the stripe.
statusLinetruePin <emoji> <project> as the mod's status entry on every surface.

Limitations

  • Claude Code's own prompt border and theme are not recoloured. The theme is global to Claude Code, and a mod can't set it per session. Only the stripe, the status line and the /color output use the project colour.
  • The stripe isn't drawn on mobile. The Claude mobile app doesn't raise the band above the prompt, so the status line's colour emoji is used instead. There are only nine coloured circle emoji, so several colours share one (teal, green and lime are all 🟢; pink shares 🔴 and magenta shares 🟣). A custom hex gets the emoji of its nearest palette colour.
  • Exact colours depend on the terminal. In a terminal without truecolor support, hex colours are approximated.
  • When nothing else draws in the band, the engine's own (empty) band drawing is left out, and only the stripe is returned.
  • Identity is the folder name, so two different repos with the same folder name get the same automatic colour. Pin one with /color to tell them apart.
Source 3 files
hooks/register.tsx 239 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, PluginOptions, Register, RenderElement, RenderInputOf, RenderSurface } from 'claude-code'
3
4import type { ProjectColorProject } from '../types'
5import {
6  basename,
7  branchFromHead,
8  commandText,
9  gitdirFromFile,
10  packPalette,
11  parseArg,
12  parseSummary,
13  resolveSwatch,
14  statusText,
15  stripeParts,
16  textOn,
17} from './lib'
18
19const project = atom({ plugin: 'project-color', key: 'project' } as const, null)
20
21/** A `CommandOutput` row's render input, on any surface. */
22type OutputInput = { [S in RenderSurface]: RenderInputOf<'CommandOutput', S> }[RenderSurface]
23
24type Config = { stripe: boolean; statusLine: boolean; branch: boolean; thickness: number }
25
26/** Where an override is kept: one per repository root. */
27const storeKey = (root: string) => `override:${root}`
28
29/** Runs `work`, swallowing a refusal: a missing piece never stops the mod. */
30async function quietly<T>(work: () => Promise<T>): Promise<T | undefined> {
31  try {
32    return await work()
33  } catch {
34    return undefined
35  }
36}
37
38/** The checked-out branch, read from `.git/HEAD` (a worktree's `.git` file followed); null when not cheap to know. */
39async function readBranch($: EngineInterface, root: string): Promise<string | null> {
40  const dir = root.replace(/[\\/]+$/, '')
41  const head = await quietly(() => $.fs.read(`${dir}/.git/HEAD`))
42  if (head !== undefined) return branchFromHead(head)
43  const file = await quietly(() => $.fs.read(`${dir}/.git`))
44  const gitdir = file === undefined ? null : gitdirFromFile(file, dir)
45  if (gitdir === null) return null
46  const linked = await quietly(() => $.fs.read(`${gitdir}/HEAD`))
47  return linked === undefined ? null : branchFromHead(linked)
48}
49
50/** Who this session's project is: the repo root's basename, else the session root's, else the cwd's. */
51async function identify($: EngineInterface, cwd: string, cfg: Config): Promise<ProjectColorProject> {
52  const repo = await quietly(() => $.session.repo())
53  const sessionRoot = await quietly(() => $.session.root())
54  const root = repo?.root ?? sessionRoot ?? cwd
55  const stored = await quietly(() => $.store.get(storeKey(root)))
56  const override = typeof stored === 'string' ? stored : null
57  // The session root is the working tree a worktree session is in; the repo root is the main one.
58  const branch = cfg.branch ? await readBranch($, sessionRoot ?? root) : null
59  return { name: basename(root), root, branch, override }
60}
61
62/** Pins `🟣 modemon` under the prompt, on every surface (it is the phone's only sign of the colour). */
63function syncStatus($: EngineInterface, p: ProjectColorProject | null, cfg: Config): void {
64  if (!cfg.statusLine || p === null) return
65  $.ui.status(statusText(p.name, resolveSwatch(p.name, p.override)))
66}
67
68/** `/color [name|#hex|auto]`: shows, pins or resets the colour, answering the row `drawOutput` draws. */
69async function runColor($: EngineInterface, args: string, cfg: Config): Promise<{ text: string }> {
70  let p = await read($, project)
71  if (p === null) {
72    p = await identify($, await $.session.cwd(), cfg)
73    await update($, project, () => p)
74  }
75  const arg = parseArg(args)
76  if (arg.kind === 'error') return { text: arg.message }
77
78  let note: string | undefined
79  if (arg.kind === 'auto') {
80    await $.store.delete(storeKey(p.root))
81    p = { ...p, override: null }
82    note = 'Back to the automatic colour.'
83  } else if (arg.kind === 'set') {
84    await $.store.set(storeKey(p.root), arg.override)
85    p = { ...p, override: arg.override }
86    note = `Pinned for ${p.root}.`
87  }
88  if (arg.kind !== 'show') {
89    const next = p
90    await update($, project, () => next)
91    syncStatus($, next, cfg)
92  }
93  return { text: commandText(p.name, resolveSwatch(p.name, p.override), p.override, note) }
94}
95
96// The command's row drawn as a tree: the current colour, then the palette as swatches.
97async function drawOutput($: EngineInterface, e: OutputInput): Promise<RenderElement | null> {
98  const summary = parseSummary(e.props.text)
99  if (e.props.isErrored || summary === null) return null
100
101  const { Box, Text } = $.ui.resolve(e)
102  const width = Math.max(16, e.viewport?.columns ?? 60)
103  const fg = textOn(summary.hex)
104  const rows = packPalette(width)
105
106  return (
107    <Box flexDirection="column">
108      <Box flexDirection="row">
109        <Text backgroundColor={summary.hex} color={fg} bold wrap="truncate-end">
110          {` ▌ ${summary.project} `}
111        </Text>
112        <Text bold> {summary.name}</Text>
113        <Text dimColor>
114          {' '}
115          {summary.hex} · {summary.source}
116        </Text>
117      </Box>
118      {rows.map(row => (
119        <Box flexDirection="row" columnGap={2}>
120          {row.map(s => {
121            const isCurrent = s.name === summary.name
122            return (
123              <Text>
124                <Text backgroundColor={s.hex} color={s.fg}>
125                  {isCurrent ? ' ● ' : '   '}
126                </Text>
127                <Text bold={isCurrent} dimColor={!isCurrent}>
128                  {' '}
129                  {s.name}
130                </Text>
131              </Text>
132            )
133          })}
134        </Box>
135      ))}
136      <Text dimColor wrap="truncate-end">
137        /color &lt;name|#hex&gt; pins · /color auto resets
138      </Text>
139    </Box>
140  )
141}
142
143export const register: Register = (on, options: PluginOptions) => {
144  const cfg: Config = {
145    stripe: options['stripe'] !== false,
146    statusLine: options['statusLine'] !== false,
147    branch: options['branch'] !== false,
148    thickness: options['thickness'] === '2' ? 2 : 1,
149  }
150
151  on('session.start', async ($, e, next) => {
152    // `/color` is the short name; `/project-color` always registers, in case a build owns `/color`.
153    await quietly(() =>
154      $.command.register({
155        name: 'color',
156        description: "Show or pin this project's colour (stripe above the prompt)",
157        argumentHint: '[name|#hex|auto]',
158        immediate: true,
159      }),
160    )
161    await quietly(() =>
162      $.command.register({
163        name: 'project-color',
164        description: "Show or pin this project's colour (same as /color)",
165        argumentHint: '[name|#hex|auto]',
166        immediate: true,
167      }),
168    )
169    const p = await quietly(() => identify($, e.cwd, cfg))
170    if (p !== undefined) {
171      await update($, project, () => p)
172      syncStatus($, p, cfg)
173    }
174    return next(e)
175  })
176
177  // The branch can move during a session; one small read per turn keeps the hint honest.
178  on('turn.complete', async ($, e, next) => {
179    if (cfg.branch && cfg.stripe) {
180      await quietly(async () => {
181        const p = await read($, project)
182        if (p === null) return
183        const branch = await readBranch($, (await quietly(() => $.session.root())) ?? p.root)
184        if (branch !== p.branch) await update($, project, was => (was === null ? was : { ...was, branch }))
185      })
186    }
187    return next(e)
188  })
189
190  on('command.run', { command: 'color' }, ($, e) => runColor($, e.args, cfg))
191  on('command.run', { command: 'project-color' }, ($, e) => runColor($, e.args, cfg))
192  on('ui.render', { component: 'CommandOutput', props: { command: 'color' } }, async ($, e, next) => {
193    return (await drawOutput($, e)) ?? next(e)
194  })
195  on('ui.render', { component: 'CommandOutput', props: { command: 'project-color' } }, async ($, e, next) => {
196    return (await drawOutput($, e)) ?? next(e)
197  })
198
199  // The stripe: drawn on top, then whatever the other band mods draw beneath it.
200  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
201    if (e.props.hasSurvey || !cfg.stripe) return next(e)
202    const p = await read($, project)
203    if (p === null) return next(e)
204
205    const { Box, Text } = $.ui.resolve(e)
206    const swatch = resolveSwatch(p.name, p.override)
207    const width = Math.max(1, e.props.bodyColumns)
208    const parts = stripeParts(p.name, p.branch, width)
209    const rows = cfg.thickness === 2 && e.props.maxRows >= 3 ? 2 : 1
210    const paint = { backgroundColor: swatch.hex, color: swatch.fg }
211
212    const stripe = (
213      <Box flexDirection="column" width={width}>
214        <Box flexDirection="row" width={width}>
215          <Text {...paint} bold wrap="truncate-end">
216            {parts.left}
217          </Text>
218          <Text {...paint}>{parts.fill}</Text>
219          {parts.right !== '' && (
220            <Text {...paint} dimColor>
221              {parts.right}
222            </Text>
223          )}
224        </Box>
225        {rows === 2 && <Text {...paint}>{' '.repeat(width)}</Text>}
226      </Box>
227    )
228
229    const below: RenderElement | null | undefined = await next(e)
230    if (below === null || below === undefined || below.type === 'engine') return stripe
231    return (
232      <Box flexDirection="column">
233        {stripe}
234        {below}
235      </Box>
236    )
237  })
238}
239
hooks/lib.ts 234 lines
1import type { ProjectColorSwatch } from '../types'
2
3/** One palette entry before its text colour is worked out. */
4type Entry = { name: string; hex: string; emoji: string }
5
6/**
7 * Twelve mid-tone colours: each is light enough to stand out on a dark
8 * terminal and saturated enough to stand out on a light one.
9 */
10const ENTRIES: readonly Entry[] = [
11  { name: 'purple', hex: '#8B5CF6', emoji: '🟣' },
12  { name: 'blue', hex: '#3B82F6', emoji: '🔵' },
13  { name: 'teal', hex: '#0D9488', emoji: '🟢' },
14  { name: 'green', hex: '#22C55E', emoji: '🟢' },
15  { name: 'lime', hex: '#84CC16', emoji: '🟢' },
16  { name: 'yellow', hex: '#EAB308', emoji: '🟡' },
17  { name: 'amber', hex: '#B45309', emoji: '🟤' },
18  { name: 'orange', hex: '#F97316', emoji: '🟠' },
19  { name: 'red', hex: '#DC2626', emoji: '🔴' },
20  { name: 'pink', hex: '#EC4899', emoji: '🔴' },
21  { name: 'magenta', hex: '#C026D3', emoji: '🟣' },
22  { name: 'slate', hex: '#64748B', emoji: '⚫' },
23]
24
25export const BLACK = '#000000'
26export const WHITE = '#FFFFFF'
27
28/** `#RRGGBB` to its three channels, 0 to 255. */
29export function rgb(hex: string): [number, number, number] {
30  const n = parseInt(hex.slice(1), 16)
31  return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]
32}
33
34/** WCAG relative luminance, 0 (black) to 1 (white). */
35export function luminance(hex: string): number {
36  const [r, g, b] = rgb(hex).map(c => {
37    const s = c / 255
38    return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4
39  }) as [number, number, number]
40  return 0.2126 * r + 0.7152 * g + 0.0722 * b
41}
42
43/** WCAG contrast ratio between two colours, 1 to 21. */
44export function contrast(a: string, b: string): number {
45  const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x) as [number, number]
46  return (hi + 0.05) / (lo + 0.05)
47}
48
49/** Black or white, whichever has the better contrast on `hex`. */
50export function textOn(hex: string): string {
51  return contrast(hex, BLACK) >= contrast(hex, WHITE) ? BLACK : WHITE
52}
53
54export const PALETTE: readonly ProjectColorSwatch[] = ENTRIES.map(e => ({ ...e, fg: textOn(e.hex) }))
55
56/** 32-bit FNV-1a over the string's UTF-16 code units: stable across runs and machines. */
57export function fnv1a(text: string): number {
58  let hash = 0x811c9dc5
59  for (let i = 0; i < text.length; i++) {
60    hash ^= text.charCodeAt(i)
61    hash = Math.imul(hash, 0x01000193)
62  }
63  return hash >>> 0
64}
65
66/** The palette colour a project gets when nothing is pinned. */
67export function autoSwatch(project: string): ProjectColorSwatch {
68  return PALETTE[fnv1a(project) % PALETTE.length] as ProjectColorSwatch
69}
70
71/** `#abc`, `#aabbcc` or `aabbcc` as `#AABBCC`; null when it is not a hex colour. */
72export function parseHex(input: string): string | null {
73  const text = input.trim()
74  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(text) ?? /^([0-9a-f]{6})$/i.exec(text)
75  const digits = m?.[1]
76  if (digits === undefined) return null
77  const full = digits.length === 3 ? [...digits].map(d => d + d).join('') : digits
78  return `#${full.toUpperCase()}`
79}
80
81/** The palette entry named `name` (any case), if any. */
82export function byName(name: string): ProjectColorSwatch | undefined {
83  const key = name.trim().toLowerCase()
84  return PALETTE.find(s => s.name === key)
85}
86
87/** The palette entry closest to `hex` by RGB distance. */
88export function nearest(hex: string): ProjectColorSwatch {
89  const [r, g, b] = rgb(hex)
90  let best = PALETTE[0] as ProjectColorSwatch
91  let bestDistance = Infinity
92  for (const s of PALETTE) {
93    const [sr, sg, sb] = rgb(s.hex)
94    const d = (r - sr) ** 2 + (g - sg) ** 2 + (b - sb) ** 2
95    if (d < bestDistance) [best, bestDistance] = [s, d]
96  }
97  return best
98}
99
100/** A custom hex as a swatch: its own text colour, the nearest palette emoji. */
101export function customSwatch(hex: string): ProjectColorSwatch {
102  return { name: 'custom', hex, fg: textOn(hex), emoji: nearest(hex).emoji }
103}
104
105/** The colour a project draws in: its pinned override when valid, else its auto colour. */
106export function resolveSwatch(project: string, override: string | null): ProjectColorSwatch {
107  if (override !== null) {
108    const named = byName(override)
109    if (named !== undefined) return named
110    const hex = parseHex(override)
111    if (hex !== null) return customSwatch(hex)
112  }
113  return autoSwatch(project)
114}
115
116export type ColorArg =
117  | { kind: 'show' }
118  | { kind: 'auto' }
119  | { kind: 'set'; override: string }
120  | { kind: 'error'; message: string }
121
122/** What `/color <args>` asks for. */
123export function parseArg(args: string): ColorArg {
124  const text = args.trim()
125  if (text === '' || /^(list|show|palette)$/i.test(text)) return { kind: 'show' }
126  if (/^(auto|reset|clear)$/i.test(text)) return { kind: 'auto' }
127  const named = byName(text)
128  if (named !== undefined) return { kind: 'set', override: named.name }
129  const hex = parseHex(text)
130  if (hex !== null) return { kind: 'set', override: hex }
131  return {
132    kind: 'error',
133    message: `"${text}" is not a palette colour or a hex colour (#RGB or #RRGGBB). Colours: ${PALETTE.map(s => s.name).join(', ')}.`,
134  }
135}
136
137/** The last path segment, trailing slashes ignored; `/` for the root. */
138export function basename(path: string): string {
139  const parts = path.split(/[\\/]+/).filter(p => p !== '')
140  return parts.at(-1) ?? '/'
141}
142
143/** The branch a `.git/HEAD` names, or a short commit when detached; null when unreadable. */
144export function branchFromHead(head: string): string | null {
145  const text = head.trim()
146  const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(text)
147  if (ref?.[1] !== undefined) return ref[1]
148  if (/^[0-9a-f]{7,64}$/i.test(text)) return text.slice(0, 7)
149  return null
150}
151
152/** The git directory a worktree's `.git` file points at (`gitdir: <path>`), resolved against `root`. */
153export function gitdirFromFile(text: string, root: string): string | null {
154  const m = /^gitdir:\s*(.+)$/m.exec(text.trim())
155  const dir = m?.[1]?.trim()
156  if (dir === undefined || dir === '') return null
157  return dir.startsWith('/') || /^[a-z]:[\\/]/i.test(dir) ? dir : `${root.replace(/[\\/]+$/, '')}/${dir}`
158}
159
160/** The status line: `🟣 modemon`. */
161export function statusText(project: string, swatch: ProjectColorSwatch): string {
162  return `${swatch.emoji} ${project}`
163}
164
165export type StripeParts = { left: string; fill: string; right: string }
166
167/**
168 * The stripe's one row as three runs exactly `width` cells wide: the project
169 * on the left (cut to fit), the hint on the right when it fits, spaces between.
170 */
171export function stripeParts(project: string, hint: string | null, width: number): StripeParts {
172  const room = Math.max(0, width)
173  let left = ` ▌ ${project} `
174  if (left.length > room) left = room <= 4 ? left.slice(0, room) : `${left.slice(0, room - 2)}… `
175  const right = hint !== null && hint !== '' ? ` ${hint} ` : ''
176  const showRight = right !== '' && left.length + right.length + 2 <= room
177  const used = left.length + (showRight ? right.length : 0)
178  return { left, fill: ' '.repeat(Math.max(0, room - used)), right: showRight ? right : '' }
179}
180
181/** Where the colour came from, as the command says it. */
182export function sourceOf(override: string | null): 'auto' | 'pinned' {
183  return override === null ? 'auto' : 'pinned'
184}
185
186/**
187 * The command's text: what the model reads and what a surface without the
188 * tree shows. Its first line is the one `parseSummary` reads back.
189 */
190export function commandText(
191  project: string,
192  swatch: ProjectColorSwatch,
193  override: string | null,
194  note?: string,
195): string {
196  const lines = [
197    `${project}: ${swatch.name} ${swatch.hex} (${sourceOf(override)})`,
198    ...(note !== undefined ? [note] : []),
199    `Palette: ${PALETTE.map(s => s.name).join(', ')}.`,
200    'Use /color <name|#hex> to pin a colour for this repo, /color auto to go back to the automatic one.',
201  ]
202  return lines.join('\n')
203}
204
205export type Summary = { project: string; name: string; hex: string; source: 'auto' | 'pinned' }
206
207/** Reads back `commandText`'s first line; null for any other text (an error, say). */
208export function parseSummary(text: string): Summary | null {
209  const first = text.split('\n')[0] ?? ''
210  const m = /^(.+): ([a-z]+) (#[0-9A-F]{6}) \((auto|pinned)\)$/.exec(first)
211  if (m === null) return null
212  const [, project, name, hex, source] = m as unknown as [string, string, string, string, 'auto' | 'pinned']
213  return { project, name, hex, source }
214}
215
216/** Palette swatches packed into rows no wider than `width` (each item `swatchWidth` cells plus its name). */
217export function packPalette(width: number, swatchWidth = 3): ProjectColorSwatch[][] {
218  const rows: ProjectColorSwatch[][] = []
219  let row: ProjectColorSwatch[] = []
220  let used = 0
221  for (const s of PALETTE) {
222    const size = swatchWidth + 1 + s.name.length + 2
223    if (row.length > 0 && used + size > width) {
224      rows.push(row)
225      row = []
226      used = 0
227    }
228    row.push(s)
229    used += size
230  }
231  if (row.length > 0) rows.push(row)
232  return rows
233}
234
types/index.d.ts 32 lines
1/** One colour of the palette, or a pinned custom hex. */
2export type ProjectColorSwatch = {
3  /** A palette name (`purple`), or `custom` for a pinned hex. */
4  name: string
5  /** `#RRGGBB`, upper case. */
6  hex: string
7  /** Black or white: whichever reads better on `hex`. */
8  fg: string
9  /** The coloured circle the status line shows (mobile has no stripe). */
10  emoji: string
11}
12
13/** The session's project as the stripe and status line draw it. */
14export type ProjectColorProject = {
15  /** The repository root's basename (the working directory's outside a repo). */
16  name: string
17  /** The absolute root the override is stored under. */
18  root: string
19  /** The checked-out branch, or a short commit when detached; null when unknown. */
20  branch: string | null
21  /** The pinned colour (`red`, `#12AB34`); null when picked automatically. */
22  override: string | null
23}
24
25declare module 'claude-code' {
26  interface PluginState {
27    'project-color': {
28      project: ProjectColorProject | null
29    }
30  }
31}
32