SLOPSHOPPER

tmux-session-rename

Names the tmux window after the conversation: on /rename, and when a named session starts or resumes

newprocess
A shopper browsing a rack in a slop shop
README

jindan-cc-marketplace

Personal Claude Code mods.

Install

One mod, in one step:

/plugin install statusline --marketplace sidhantunnithan/jindan-cc-marketplace

Or add the marketplace once, then install any mod from it:

/plugin marketplace add sidhantunnithan/jindan-cc-marketplace
/plugin install <mod>@jindan

Mods

ModWhat it does
statuslineStatus line in the prompt footer. At the left, the model (Opus 5.5 (1M context)) and working directory (~/Projects/foo, cut from the left past 40 characters). At the right, a context window bar (▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▒▒▒ 32%) coloured by what fills the context. simple (default): grey for system (prompt, tools, MCP, commands), blue for custom (agents, memory files, skills), yellow for messages. granular: each category in its /context colour. Free space is ▱, the autocompact buffer ▒; the percentage is green below 60%, yellow below 80%, red above. A shorthand line under the bar names each segment (sys 16k · custom 3k · msgs 92k). In /config: Colour mode, and Show model / Show directory / Show context bar.
tmux-session-renameNames the tmux window after the conversation. /rename <name> renames it at once; a bare /rename takes the name Claude generates; resuming a named session (--resume, --continue) names it as the session starts. When the session ends (exit or /clear) the window goes back to tmux's automatic name. Does nothing outside tmux; needs no tmux.conf changes.

Adding a mod

  1. Put it in its own folder with .claude-plugin/plugin.json and hooks/.
  2. List it under plugins in .claude-plugin/marketplace.json.
  3. claude plugin validate . and claude plugin test <mod>.
Source 1 files
hooks/register.tsx 82 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3// What /rename prints, saved as the command's output row. A bare /rename names the
4// session itself, and this row is the only place the plugin can read that name.
5const RENAMED = /^<local-command-stdout>Session renamed to: (.+)<\/local-command-stdout>$/s
6
7const renamedTo = (content: unknown) => {
8  const text = Array.isArray(content)
9    ? content.map(block => (block?.type === 'text' ? block.text : '')).join('')
10    : typeof content === 'string' ? content : ''
11  return RENAMED.exec(text.trim())?.[1]?.trim()
12}
13
14// Outside tmux there is no window to name, so every call is a no-op.
15async function nameWindow($: EngineInterface, title: string) {
16  const pane = await $.env.get('TMUX_PANE')
17  if (pane) await $.process.run(['tmux', 'rename-window', '-t', pane, title])
18}
19
20// The session's name as /rename last set it: the newest custom-title row of its
21// transcript. classic.SessionStart would carry it, but plugins at the user tier
22// are bypassed on that event, so the hook never runs in a real session.
23async function sessionTitle($: EngineInterface) {
24  const id = await $.session.id()
25  const { stdout } = await $.process.run([
26    'sh', '-c',
27    'cat "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/projects/*/"$1".jsonl 2>/dev/null | grep \'"type":"custom-title"\' | tail -n 1',
28    'sh', id,
29  ])
30  if (!stdout.trim()) return undefined
31  return (JSON.parse(stdout) as { customTitle?: string }).customTitle?.trim() || undefined
32}
33
34// rename-window turns automatic-rename off for the window; turning it back on
35// hands the name back to tmux (the running command) once the session is over.
36async function releaseWindow($: EngineInterface) {
37  const pane = await $.env.get('TMUX_PANE')
38  if (pane) await $.process.run(['tmux', 'set-option', '-w', '-t', pane, 'automatic-rename', 'on'])
39}
40
41export const register: Register = on => {
42  // /rename <name>: the name is in the args, so the window follows at once.
43  on('command.run', { command: 'rename' }, async ($, e, next) => {
44    const result = await next(e)
45    const title = e.args.trim()
46    if (title) await nameWindow($, title)
47    return result
48  }).catch(($, e, next) => next(e))
49
50  // Bare /rename: read the generated name off the command's output row.
51  on('session.append', { door: 'command' }, async ($, e, next) => {
52    const result = await next(e)
53    const title = renamedTo(e.message.content)
54    if (title) await nameWindow($, title)
55    return result
56  }).catch(($, e, next) => next(e))
57
58  // /resume: the session.end it raises (reason resume) has just handed the window
59  // back to tmux, so name it after the session now running.
60  on('command.run', { command: 'resume' }, async ($, e, next) => {
61    const result = await next(e)
62    const title = await sessionTitle($)
63    if (title) await nameWindow($, title)
64    return result
65  }).catch(($, e, next) => next(e))
66
67  // claude --resume / --continue: a named session names the window as it starts.
68  on('session.start', async ($, e, next) => {
69    const result = await next(e)
70    const title = await sessionTitle($)
71    if (title) await nameWindow($, title)
72    return result
73  }).catch(($, e, next) => next(e))
74
75  // Covers exit and /clear, after which the conversation has no name of its own.
76  on('session.end', async ($, e, next) => {
77    const result = await next(e)
78    await releaseWindow($)
79    return result
80  }).catch(($, e, next) => next(e))
81}
82