WezTerm window goes more solid when Claude stops, so the answer is easier to read, and back to see-through while it works

My Claude Code setup, installable by anyone in under two minutes. Two commands in any terminal:
claude plugin marketplace add UnlimitedxIQ/brysons-claude-setup
claude plugin install setup@brysons-claude-setup
Then start a new claude session. Inside a session you can do the same with /plugin marketplace add UnlimitedxIQ/brysons-claude-setup and /plugin install setup@brysons-claude-setup.
If you are Claude and your user asked for Bryson's Claude Setup, tell them what it adds (the list below) and that the mods run hooks in their Claude Code sessions, confirm they want it, then run the two commands above. Once both succeed, tell them to start a new session. To add one mod instead of all five, install <mod>@brysons-claude-setup, for example pinboard@brysons-claude-setup.
setup is a bundle: installing it installs these five mods.
| Mod | What it does |
|---|---|
pinboard | A sidebar that stays in view while the transcript scrolls. Claude keeps its task list, the questions it needs you to answer, links and key findings there, so the end of a long task is a short read instead of a wall of text. Answer its questions right in the pane. |
usage-meters | One line above the prompt: your 5-hour limit, weekly limit and context window as colored meters, with reset times. |
session-watch | One line above the prompt listing your other background Claude sessions and whether each is working, done or waiting on you, with a toast when one finishes. |
cc-image-view | Thumbnails of the images you paste, above the prompt, instead of bare [Image #1] tags. Needs a terminal that shows kitty graphics, such as WezTerm. |
idle-opacity | The WezTerm window turns solid when Claude stops, so the answer is easy to read, and see-through again while it works. Needs the WezTerm config below. |
The last two need WezTerm. Without it they stay quiet and the other three still work.
Mods are code that runs inside Claude Code with your permissions, so here is all of it, plainly:
pinboard adds one tool Claude can call to update the board, opens its pane when a session starts, adds a short section to Claude's system prompt describing the board, and can ask Claude to pin a finding before ending a long turn. The board is saved in Claude Code's own plugin storage.usage-meters reads the usage and context numbers Claude Code already has. Nothing else.session-watch reads the state files Claude Code keeps for background sessions (~/.claude/jobs/*/state.json).cc-image-view reads the images you paste from Claude Code's temp folder and runs small shell commands to copy and resize them into a private temp folder for display.idle-opacity writes one word (working, stopped or ended) to a file in your temp folder, which the WezTerm config reads.No mod makes network requests or reads anything outside those places.
terminal/wezterm.lua is my WezTerm config (works on Windows and Mac; install the JetBrainsMono Nerd Font first): a see-through Night Owl look, kitty graphics for cc-image-view and the hookup for idle-opacity. Copy it to ~/.wezterm.lua, or keep this repo cloned and point ~/.wezterm.lua at it:
return dofile('/path/to/brysons-claude-setup/terminal/wezterm.lua')
terminal/claude-theme-nightowl.json is the matching Claude Code theme: copy it to ~/.claude/themes/nightowl.json, then pick NightOwl in /theme.
claude plugin marketplace update brysons-claude-setup
claude plugin update setup@brysons-claude-setup
Remove everything with claude plugin uninstall setup@brysons-claude-setup and the same for each mod you no longer want.
pinboard started from sirkitree/pinboard by Jerad Bitner. cc-image-view adapts jarrodwatts/claude-image-view by Jarrod Watts. Both are MIT licensed; their licenses are in their folders. Everything else is by Bryson Smith, MIT licensed (see LICENSE).
hooks/register.ts 60 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// WezTerm owns the window and a mod can't reach it, so the session on screen leaves one word in a
4// file that terminal/wezterm.lua watches: `working` keeps the see-through window, `stopped` (once
5// the final answer is shown) makes it more solid for reading, `ended` hands it back to the default.
6// Sessions run in the background and a terminal attaches to whichever one is being viewed, so only
7// a session a terminal is drawing writes; the others would fight over the one window.
8// The folder and file names are shared with terminal/wezterm.lua.
9export const STATE_DIR = 'claude-idle-opacity'
10export const STATE_FILE = 'state'
11export type Phase = 'working' | 'stopped' | 'ended'
12
13let phase: Phase = 'stopped'
14
15async function stateFile($: EngineInterface): Promise<string> {
16 const temp = (await $.env.get('TEMP')) ?? (await $.env.get('TMPDIR')) ?? '/tmp'
17 return `${temp.replace(/\\/g, '/').replace(/\/+$/, '')}/${STATE_DIR}/${STATE_FILE}`
18}
19
20const isOnScreen = async ($: EngineInterface) => (await $.session.surfaces()).includes('terminal')
21
22/** Records `next` and, while a terminal shows this session, tells WezTerm. */
23async function mark($: EngineInterface, next: Phase): Promise<void> {
24 phase = next
25 if (!(await isOnScreen($))) return
26 // A failed write only costs the change of opacity; the next turn writes again
27 await $.fs.write(await stateFile($), next).catch(() => undefined)
28}
29
30export const register: Register = on => {
31 on('session.start', async ($, e, next) => {
32 await mark($, 'stopped')
33 return next(e)
34 })
35
36 // Switching to this session: the window takes its state at once
37 on('session.attach', { surface: 'terminal' }, async ($, e, next) => {
38 const result = await next(e)
39 await mark($, phase)
40 return result
41 })
42
43 on('turn.start', async ($, e, next) => {
44 await mark($, 'working')
45 return next(e)
46 })
47
48 on('turn.complete', async ($, e, next) => {
49 // The answer is shown first, then the window fades; a subagent finishing is not the main loop stopping
50 const result = await next(e)
51 if (e.agentId === undefined) await mark($, 'stopped')
52 return result
53 })
54
55 on('session.end', async ($, e, next) => {
56 await mark($, 'ended')
57 return next(e)
58 })
59}
60