SLOPSHOPPER

everforest-zen

A calm Everforest tree above your Claude Code prompt

newbandspinnerguardtoastprompt
v1.0.0MITupdated 2026-10-13MarDevMint/everforest-zen
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · everforest-zen
› 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 Oo. ° Breathing ° .oO (0s) -.@@@&@@@@@@#-. * (.@@&@@@@@@@@@@&#####.) ((@@@@@@@@@@########%%#))  files cache.ts · audit.ts +1 (@@@@&@\@@#####/#%%%%%)  context ━━━━──── 49% ''##@@\%|%/##%%''  session ━━────── 31% NaNm \|/  time 30m (|) ~,~~~~~~~_,/(|)\,_~~~',',,' ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
-.@@@&@@@@@@#-. * (.@@&@@@@@@@@@@&#####.) ((@@@@@@@@@@########%%#))  files cache.ts · audit.ts +1 (@@@@&@\@@#####/#%%%%%)  context ━━━━──── 49% ''##@@\%|%/##%%''  session ━━────── 31% NaNm \|/  time 30m (|) ~,~~~~~~~_,/(|)\,_~~~',',,'
Spinner
Oo. ° Breathing ° .oO (0s)
README

everforest-zen

A calm Everforest tree above your Claude Code prompt.

everforest-zen

  • the crown sways while Claude works, turns cyan with subagents and blue on the web
  • context and session use at a glance
  • follows your clock: lighter by day, darker at night, fireflies after dusk
  • blooms like a cherry tree when the context runs high
  • soft wind when a turn ends, a small bird when Claude needs you
  • a gentle reminder to breathe every 50 minutes

Install

/plugin install everforest-zen --marketplace MarDevMint/everforest-zen

Needs Claude Code with mods (2.1.287+). Sounds play on Linux with PipeWire or PulseAudio.

Settings

Sounds, icons, the breathe reminder and the bloom threshold are in /config.

Make it yours

Everything lives in hooks/register.tsx: the palette, the tree, the words. Fork it, edit, and run claude --plugin-dir ./everforest-zen to try your changes.

MIT

Source 2 files
hooks/register.tsx 553 lines
1import { atom, read, update } from 'claude-code'
2import type { Register, SessionRateLimit } from 'claude-code'
3
4import type { Limits } from '../types'
5
6const tick = atom({ plugin: 'everforest-zen', key: 'tick' } as const, 0)
7const limits = atom({ plugin: 'everforest-zen', key: 'limits' } as const, null)
8const files = atom({ plugin: 'everforest-zen', key: 'files' } as const, [])
9const startedAt = atom({ plugin: 'everforest-zen', key: 'startedAt' } as const, null)
10const agents = atom({ plugin: 'everforest-zen', key: 'agents' } as const, 0)
11const activity = atom({ plugin: 'everforest-zen', key: 'activity' } as const, 0)
12const turnStart = atom({ plugin: 'everforest-zen', key: 'turnStart' } as const, 0)
13const lastBreath = atom({ plugin: 'everforest-zen', key: 'lastBreath' } as const, 0)
14const restUntil = atom({ plugin: 'everforest-zen', key: 'restUntil' } as const, 0)
15const bloomNoted = atom({ plugin: 'everforest-zen', key: 'bloomNoted' } as const, false)
16const context = atom({ plugin: 'everforest-zen', key: 'context' } as const, null)
17
18// settings, from the plugin's userConfig (see plugin.json); set in register
19let NERD = true
20let SOUND = true
21let BLOOM_AT = 80
22let BREATHE_EVERY = 50
23const REST_MS = 60_000
24
25// Everforest dark palette
26const C = {
27  far: '#3f5245',
28  back: '#56705a',
29  deep: '#4a6350',
30  mid: '#7d9a6b',
31  light: '#a7c080',
32  aqua: '#83c092',
33  trunk: '#7a6d5a',
34  bark: '#9a8a70',
35  grass: '#859289',
36  fg: '#d3c6aa',
37  yellow: '#dbbc7f',
38  blue: '#7fbbb3',
39  pink: '#d699b6',
40  moss: '#b5b27a',
41  brown: '#a68b5b',
42  earth: '#8c6a4a',
43  empty: '#4f585e',
44  hint: '#7a8478',
45  firefly: '#dbbc7f',
46  glowLeaf: '#c9d9a0',
47}
48
49const ROWS = 8
50const TW = 27
51const GAP = 3
52
53type Cell = { ch: string; color: string; glow?: boolean }
54type Seg = { text: string; color: string }
55
56const noise = (x: number, y: number, salt = 0) => {
57  let n = Math.imul(x + 11, 73856093) ^ Math.imul(y + 7, 19349663) ^ Math.imul(salt + 3, 83492791)
58  n = Math.imul(n ^ (n >>> 13), 1274126177)
59  return ((n ^ (n >>> 16)) >>> 0) % 1000 / 1000
60}
61
62// 1 by day, 0.4 at night, ramping through dawn (5-8h) and dusk (18-21h)
63const brightness = (h: number): number => {
64  if (h >= 8 && h < 18) return 1
65  if (h >= 18 && h < 21) return 1 - ((h - 18) / 3) * 0.6
66  if (h >= 5 && h < 8) return 0.4 + ((h - 5) / 3) * 0.6
67  return 0.4
68}
69
70// darken a colour toward the Everforest bg, so night stays green, only deeper
71const dim = (hex: string, f: number): string => {
72  if (f >= 1) return hex
73  const bg = [43, 51, 57]
74  const out = [1, 3, 5].map((i, k) => {
75    const c = parseInt(hex.slice(i, i + 2), 16)
76    return Math.round(bg[k]! + (c - bg[k]!) * f).toString(16).padStart(2, '0')
77  })
78  return `#${out.join('')}`
79}
80
81const put = (grid: Cell[][], row: number, col: number, ch: string, color: string, glow = false) => {
82  if (row < 0 || row >= grid.length || col < 0 || col >= TW || ch === ' ') return
83  grid[row]![col] = { ch, color, glow }
84}
85
86// shimmer: a stirred leaf takes the next shade
87const STIR: Record<string, string> = { '@': '&', '#': '@', '%': '#', '&': '%' }
88
89export type Mode = 'idle' | 'work' | 'agents' | 'web' | 'rest'
90
91// the crown's three shades per mode: green at rest, cyan with company, blue on the web
92// cherry blossom, in Everforest's own pink
93const SAKURA: [string, string, string] = ['#e8b8cc', '#d699b6', '#a97a92']
94
95const CROWN: Record<Mode, [string, string, string]> = {
96  idle: ['#a7c080', '#7d9a6b', '#56705a'],
97  work: ['#a7c080', '#7d9a6b', '#56705a'],
98  agents: ['#83c092', '#5f9a84', '#3f6b5e'],
99  web: ['#7fbbb3', '#5b8f8a', '#3e6466'],
100  rest: ['#d8c48a', '#b5a46e', '#8f8158'],
101}
102
103/**
104 * One detailed tree, always full. At rest it sways; while Claude works a band
105 * of light sweeps through the crown; with subagents or on the web, a wave of
106 * cyan or blue flows through the leaves.
107 */
108const tree = (t: number, mode: Mode, hour: number, workSecs: number, blooming: boolean): Cell[][] => {
109  const grid: Cell[][] = Array.from({ length: ROWS }, () =>
110    Array.from({ length: TW }, () => ({ ch: ' ', color: C.grass })),
111  )
112  const light = brightness(hour)
113  const busy = mode !== 'idle'
114  const fill = 0.97
115  const cx = 13
116  const cy = 2.1
117  const rx = 12.2
118  const ry = 2.75
119  // once the context runs high the tree blooms: pink blossom among the green
120  const green = CROWN.idle
121  const tinted = CROWN[mode]
122
123  for (let y = 0; y < 5; y++) {
124    for (let x = 0; x < TW; x++) {
125      const dx = (x - cx) / rx
126      const dy = (y - cy) / ry
127      const d = dx * dx + dy * dy
128      if (d > 1) continue
129      const edge = d > 0.78
130      if (noise(x, y) > fill + (edge ? 0.04 : 0.1)) continue
131
132      // which palette this leaf wears now: a diagonal wave drifting through the crown
133      const inWave =
134        ((mode === 'agents' || mode === 'web') && (((x + y * 2 - t) % 12) + 12) % 12 < 7) ||
135        // time to rest: a slow warm wave, like late afternoon light
136        (mode === 'rest' && (((x - y - Math.floor(t / 2)) % 10) + 10) % 10 < 6)
137      const blossom = blooming && noise(x, y, 11) < 0.7
138      const pal = inWave ? tinted : blossom ? SAKURA : green
139      // while working, a soft band of light passes left to right
140      const lit = mode === 'work' && (((x - t) % 18) + 18) % 18 < 3
141
142      if (edge) {
143        const side = Math.abs(dx) > 0.82
144        const ch = side ? (dx < 0 ? '(' : ')') : dy < 0 ? (noise(x, y, 1) > 0.5 ? '.' : '-') : "'"
145        put(grid, y, x, ch, lit ? C.glowLeaf : pal[0])
146        continue
147      }
148      // light falls from the top left; leaves grow in clumps
149      const clump = noise(Math.floor(x / 3), Math.floor(y / 2), 2)
150      const shade = -0.6 * dx - 0.5 * dy + (clump - 0.5) * 0.9
151      const [ch, color] = shade > -0.05 ? ['@', pal[0]] : shade > -0.5 ? ['#', pal[1]] : ['%', pal[2]]
152      const stirred = (x + y * 5 + t) % (busy ? 3 : 11) === 0
153      put(grid, y, x, stirred ? (STIR[ch] ?? ch) : ch, lit ? C.glowLeaf : color)
154    }
155  }
156
157  // branches, trunk and roots
158  const bark = C.trunk
159  put(grid, 3, cx - 4, '\\', C.bark)
160  put(grid, 3, cx + 4, '/', C.bark)
161  put(grid, 4, cx - 2, '\\', C.bark)
162  put(grid, 4, cx, '|', C.bark)
163  put(grid, 4, cx + 2, '/', C.bark)
164  put(grid, 5, cx - 1, '\\', bark)
165  put(grid, 5, cx, '|', bark)
166  put(grid, 5, cx + 1, '/', bark)
167  put(grid, 6, cx - 1, '(', bark)
168  put(grid, 6, cx, '|', bark)
169  put(grid, 6, cx + 1, ')', bark)
170  '_,/(|)\\,_'.split('').forEach((ch, i) => put(grid, 7, cx - 4 + i, ch, bark))
171
172  // ground
173  for (let x = 0; x < TW; x++) {
174    if (grid[7]![x]!.ch !== ' ') continue
175    const r = noise(x, 7, 5)
176    put(grid, 7, x, r < 0.55 ? '~' : r < 0.78 ? ',' : r < 0.9 ? "'" : '"', r < 0.15 ? C.light : C.grass)
177  }
178
179  // a sprout grows at the roots while Claude works
180  if (busy) {
181    const sx = cx + 6
182    const stage = workSecs < 3 ? 0 : workSecs < 8 ? 1 : workSecs < 15 ? 2 : 3
183    const sprout = [[','], ['i'], ['|', 'Y'], ['|', 'Y', '.']][stage]!
184    sprout.forEach((ch, i) => put(grid, 7 - i, sx, ch, i === 2 ? C.glowLeaf : C.light))
185  }
186
187  // sun or moon, and fireflies once the light goes
188  const isDay = hour >= 6 && hour < 18
189  put(grid, 0, TW - 1, isDay ? '*' : ')', C.yellow, true)
190  if (light < 0.85) {
191    for (let i = 0; i < 4; i++) {
192      if ((t + i * 3) % 5 >= 3) continue
193      const x = (i * 7 + 2 + Math.floor(t / 3) * (i % 2 === 0 ? 1 : -1) + TW * 8) % TW
194      put(grid, 1 + ((i * 3 + Math.floor(t / 4)) % 5), x, '*', C.firefly, true)
195    }
196  }
197
198  for (const row of grid) {
199    for (const cell of row) {
200      if (!cell.glow) cell.color = dim(cell.color, light)
201    }
202  }
203  return grid
204}
205
206// merge neighbouring cells of one colour so a row is a handful of Text nodes
207const runs = (row: Cell[]): Seg[] => {
208  const out: Seg[] = []
209  for (const cell of row) {
210    const last = out[out.length - 1]
211    if (last && last.color === cell.color) last.text += cell.ch
212    else out.push({ text: cell.ch, color: cell.color })
213  }
214  return out
215}
216
217const toLimits = (rates: readonly SessionRateLimit[]): Limits | null => {
218  const five = rates.find((r) => r.kind === 'five_hour')
219  if (!five) return null
220  const week = rates.find((r) => r.kind === 'seven_day')
221  return { session: five.percentUsed, resetsAt: five.resetsAt ?? null, week: week ? week.percentUsed : null }
222}
223
224const left = (resetsAt: string | null, now: number): string => {
225  if (!resetsAt) return ''
226  const mins = Math.max(0, Math.round((Date.parse(resetsAt) - now) / 60000))
227  const h = Math.floor(mins / 60)
228  return h > 0 ? `${h}h ${mins % 60}m` : `${mins}m`
229}
230
231// green while there is plenty, turning to moss and then earth as it runs low
232const tint = (pct: number) => (pct >= 90 ? C.earth : pct >= 75 ? C.brown : pct >= 50 ? C.moss : C.light)
233
234// label colours fade from leaf green down to bark
235const FADE = ['#a7c080', '#93ab76', '#7d9a6b', '#8f8a68', '#9a8a70'] as const
236
237const ICON = { files: '\uf15b', context: '\uf2db', session: '\uf017', time: '\uf253', agents: '\uf1bb' }
238
239const label = (name: keyof typeof ICON, color: string): Seg => ({
240  text: `${NERD ? `${ICON[name]} ` : ''}${name.padEnd(8)}`,
241  color,
242})
243
244const bar = (pct: number, width: number): Seg[] => {
245  const filled = Math.round((Math.min(100, Math.max(0, pct)) / 100) * width)
246  return [
247    { text: '━'.repeat(filled), color: tint(pct) },
248    { text: '─'.repeat(width - filled), color: C.empty },
249  ]
250}
251
252const baseName = (path: string) => path.split('/').pop() ?? path
253
254const clip = (segs: Seg[], width: number): Seg[] => {
255  const out: Seg[] = []
256  let room = width
257  for (const s of segs) {
258    if (room <= 0) break
259    out.push(s.text.length > room ? { ...s, text: s.text.slice(0, room) } : s)
260    room -= s.text.length
261  }
262  return out
263}
264
265type Facts = {
266  lim: Limits | null
267  recent: string[]
268  ctx: number | null
269  upMins: number | null
270  running: number
271  now: number
272}
273
274const span = (mins: number) => {
275  const h = Math.floor(mins / 60)
276  return h > 0 ? `${h}h ${mins % 60}m` : `${mins}m`
277}
278
279const info = (f: Facts, width: number): Seg[][] => {
280  const barW = Math.max(4, Math.min(8, width - 26))
281  const rows: Seg[][] = []
282
283  const shown = f.recent.slice(0, 2).map(baseName)
284  const more = f.recent.length - shown.length
285  // a row shows only once it has something to say
286  if (shown.length) {
287    rows.push([
288      label('files', FADE[0]),
289      { text: shown.join(' · '), color: C.fg },
290      ...(more > 0 ? [{ text: `  +${more}`, color: C.hint }] : []),
291    ])
292  }
293
294  if (f.ctx !== null) {
295    rows.push([label('context', FADE[1]), ...bar(f.ctx, barW), { text: ` ${Math.round(f.ctx)}%`, color: tint(f.ctx) }])
296  }
297
298  if (f.lim) {
299    rows.push([
300      label('session', FADE[2]),
301      ...bar(f.lim.session, barW),
302      { text: ` ${Math.round(f.lim.session)}%`, color: tint(f.lim.session) },
303      { text: f.lim.resetsAt ? `  ${left(f.lim.resetsAt, f.now)}` : '', color: C.hint },
304    ])
305  }
306
307  if (f.upMins !== null) rows.push([label('time', FADE[3]), { text: span(f.upMins), color: C.fg }])
308
309  if (f.running > 0) rows.push([label('agents', FADE[4]), { text: String(f.running), color: C.fg }])
310  return rows.map((r) => clip(r, width))
311}
312
313const WORDS = [
314  'Breathing',
315  'Rooting',
316  'Sprouting',
317  'Blooming',
318  'Meditating',
319  'Growing',
320  'Listening',
321  'Resting',
322  'Flowing',
323  'Unfolding',
324  'Settling',
325  'Drifting',
326]
327
328// bubbles either side of the words: a slow wave swells them in turn, mirrored,
329// so the two sides rise and settle together
330const BUBBLE = [' ', '.', '°', 'o', 'O'] as const
331const BUBBLE_SHADE = ['#4a6350', '#56705a', '#6b8a62', '#7d9a6b', '#a7c080'] as const
332const BUBBLES = 7
333
334const bubbles = (t: number): { ch: string; color: string }[] =>
335  Array.from({ length: BUBBLES }, (_, i) => {
336    // i = 0 sits by the words; the swell travels outward
337    const v = (Math.sin(t * 0.5 - i * 0.7) + 1) / 2
338    const level = Math.min(BUBBLE.length - 1, Math.floor(v * BUBBLE.length))
339    return { ch: BUBBLE[level]!, color: BUBBLE_SHADE[level]! }
340  })
341
342const hash = (s: string) => [...s].reduce((a, c) => (a * 31 + c.charCodeAt(0)) % 9973, 7)
343
344export const register: Register = (on, options) => {
345  NERD = options.nerdIcons !== false
346  SOUND = options.sound !== false
347  BLOOM_AT = typeof options.bloomAt === 'number' ? options.bloomAt : 80
348  BREATHE_EVERY = typeof options.breatheEvery === 'number' ? options.breatheEvery : 50
349
350  on('session.start', async ($, e, next) => {
351    $.clock.every(700, () => {
352      void update($, tick, (n) => n + 1)
353    })
354    // every so often, a gentle nudge to breathe: a toast, the wind, warm light in the crown
355    $.clock.every(60_000, async () => {
356      if (BREATHE_EVERY <= 0) return
357      const now = await $.clock.now()
358      const last = await read($, lastBreath)
359      if (last === 0) {
360        await update($, lastBreath, () => now)
361        return
362      }
363      if (now - last < BREATHE_EVERY * 60_000) return
364      await update($, lastBreath, () => now)
365      await update($, restUntil, () => now + REST_MS)
366      $.ui.toast('🌿 take a breath · stretch a little', { timeoutMs: 12_000 })
367      if (SOUND) {
368        const file = `${$.plugin.root}/sounds/wind.wav`
369        void $.process
370          .run(['pw-play', '--volume', '0.7', file])
371          .then((r) => (r.exitCode === 0 ? r : $.process.run(['paplay', file])))
372          .catch(() => undefined)
373      }
374    })
375    $.clock.every(2000, async () => {
376      const list = await $.agent.list()
377      const running = list.filter((a) => a.status === 'running' || a.status === 'pending').length
378      await update($, agents, () => running)
379    })
380    const usage = await $.session.usage()
381    await update($, limits, () => toLimits(usage.rateLimits))
382    await update($, context, () => usage.context.percent ?? null)
383    await update($, startedAt, () => usage.startedAt)
384    return next(e)
385  })
386
387  on('session.measure', async ($, e, next) => {
388    if (e.changed.includes('rateLimits')) await update($, limits, () => toLimits(e.rateLimits))
389    if (e.changed.includes('context')) {
390      const pct = e.context.percent ?? null
391      await update($, context, () => pct)
392      // one quiet word when the tree first blooms; again only after it settles
393      const noted = await read($, bloomNoted)
394      if (pct !== null && pct >= BLOOM_AT && !noted) {
395        await update($, bloomNoted, () => true)
396        $.ui.toast('🌸 the tree is in bloom · /compact when you are ready', { timeoutMs: 10_000 })
397      } else if (pct !== null && pct < BLOOM_AT && noted) {
398        await update($, bloomNoted, () => false)
399      }
400    }
401    return next(e)
402  })
403
404  // web tools in flight: the tree turns blue while any runs
405  on('tool.call', { tool: 'WebSearch' }, async ($, e, next) => {
406    await update($, activity, (n) => n + 1)
407    try {
408      return await next(e)
409    } finally {
410      await update($, activity, (n) => Math.max(0, n - 1))
411    }
412  })
413
414  on('tool.call', { tool: 'WebFetch' }, async ($, e, next) => {
415    await update($, activity, (n) => n + 1)
416    try {
417      return await next(e)
418    } finally {
419      await update($, activity, (n) => Math.max(0, n - 1))
420    }
421  })
422
423  on('tool.call', async ($, e, next) => {
424    // bookkeeping only: it must never get in the way of the call
425    try {
426      if (e.tool === 'Edit' || e.tool === 'Write') {
427        const path = e.file_path
428        await update($, files, (list) => [path, ...list.filter((p) => p !== path)].slice(0, 20))
429      }
430    } catch {}
431    return next(e)
432  })
433
434  // Claude is waiting on you: a permission prompt or a dialog. One soft call of a bird.
435  on('classic.Notification', async ($, e, next) => {
436    if (e.notification_type === 'permission_prompt' || e.notification_type === 'elicitation_dialog') {
437      if (SOUND) {
438        const file = `${$.plugin.root}/sounds/call.wav`
439        void $.process
440          .run(['pw-play', '--volume', '0.7', file])
441          .then((r) => (r.exitCode === 0 ? r : $.process.run(['paplay', file])))
442          .catch(() => undefined)
443      }
444    }
445    return next(e)
446  })
447
448  on('tool.call', { tool: 'AskUserQuestion' }, async ($, e, next) => {
449    if (SOUND) {
450      const file = `${$.plugin.root}/sounds/call.wav`
451      void $.process
452        .run(['pw-play', '--volume', '0.7', file])
453        .then((r) => (r.exitCode === 0 ? r : $.process.run(['paplay', file])))
454        .catch(() => undefined)
455    }
456    return next(e)
457  })
458
459  // a soft gust of wind when a turn ends. The engine's own player is
460  // macOS-only, so on Linux PipeWire plays the file; elsewhere it stays quiet.
461  on('turn.complete', async ($, e, next) => {
462    if (SOUND && e.agentId === undefined) {
463      const file = `${$.plugin.root}/sounds/wind.wav`
464      void $.process
465        .run(['pw-play', '--volume', '0.7', file])
466        .then((r) => (r.exitCode === 0 ? r : $.process.run(['paplay', file])))
467        .catch(() => undefined)
468    }
469    return next(e)
470  })
471
472  on('prompt.submit', async ($, e, next) => {
473    const now = await $.clock.now()
474    await update($, turnStart, () => now)
475    return next(e)
476  })
477
478  // the spinner line drawn in green, the words floating between two rows of bubbles
479  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
480    const { Box, Text } = $.ui.resolve(e)
481    const t = await read($, tick)
482    const began = await read($, turnStart)
483    const secs = began ? Math.max(0, Math.round(((await $.clock.now()) - began) / 1000)) : 0
484    const word = e.props.message ?? WORDS[hash(e.props.word) % WORDS.length] ?? WORDS[0]!
485    const side = bubbles(t)
486    const left = [...side].reverse()
487    return (
488      <Box>
489        {left.map((b, i) => (
490          <Text key={`l${i}`} color={b.color}>
491            {b.ch}
492          </Text>
493        ))}
494        <Text color={C.mid}> {word} </Text>
495        {side.map((b, i) => (
496          <Text key={`r${i}`} color={b.color}>
497            {b.ch}
498          </Text>
499        ))}
500        <Text color={C.hint}> ({secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s`})</Text>
501      </Box>
502    )
503  })
504
505  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
506    if (e.props.hasSurvey) return next(e)
507
508    const t = await read($, tick)
509    const now = await $.clock.now()
510    const lim = await read($, limits)
511    const began = await read($, startedAt)
512    const facts: Facts = {
513      lim,
514      recent: await read($, files),
515      ctx: await read($, context),
516      running: await read($, agents),
517      upMins: began === null ? null : Math.max(0, Math.round((now - began) / 60000)),
518      now,
519    }
520    const clock = new Date(now)
521    const web = await read($, activity)
522    const resting = now < (await read($, restUntil))
523    const mode: Mode = resting ? 'rest' : web > 0 ? 'web' : facts.running > 0 ? 'agents' : e.props.isWorking ? 'work' : 'idle'
524    const began2 = await read($, turnStart)
525    const workSecs = began2 ? (now - began2) / 1000 : 0
526    const grid = tree(t, mode, clock.getHours() + clock.getMinutes() / 60, workSecs, (facts.ctx ?? 0) >= BLOOM_AT)
527    const infoWidth = Math.max(0, Math.min(e.props.bodyColumns, 100) - TW - GAP)
528    const side = info(facts, infoWidth)
529    const top = Math.max(0, Math.floor((ROWS - side.length) / 2))
530    const { Box, Text } = $.ui.resolve(e)
531
532    return (
533      <Box flexDirection="column">
534        {grid.map((row, y) => (
535          <Box key={`r${y}`}>
536            {runs(row).map((seg, i) => (
537              <Text key={`t${i}`} color={seg.color}>
538                {seg.text}
539              </Text>
540            ))}
541            {infoWidth > 0 && side[y - top] ? <Text>{' '.repeat(GAP)}</Text> : null}
542            {(side[y - top] ?? []).map((seg, i) => (
543              <Text key={`i${i}`} color={seg.color}>
544                {seg.text}
545              </Text>
546            ))}
547          </Box>
548        ))}
549      </Box>
550    )
551  })
552}
553
types/index.d.ts 20 lines
1export type Limits = { session: number; resetsAt: string | null; week: number | null }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'everforest-zen': {
6      tick: number
7      limits: Limits | null
8      files: string[]
9      startedAt: number | null
10      context: number | null
11      agents: number
12      activity: number
13      turnStart: number
14      lastBreath: number
15      restUntil: number
16      bloomNoted: boolean
17    }
18  }
19}
20