SLOPSHOPPER

context-meter

Shows the git branch, context window usage, what the last turn added, and plan limit usage above the prompt

newbandcommandprocess
v0.2.0MITupdated 2026-10-07ulttla/claude-mod-repo/context-meter
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-meter
› 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 › /context-meter ⎿ context-meter: feat/auth-refresh · Ctx 49% (97.4k/200k) · Last ±0 · 5H 31% ↻09:53 feat/auth-refresh Ctx 49% (97.4k/200k) Last ±0 5H 31% ↻09:53 ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
feat/auth-refresh Ctx 49% (97.4k/200k) Last ±0 5H 31% ↻09:53 ⟨Claude Code's own drawing⟩
README

claude-mod-repo

Small mods for Claude Code.

English · 한국어


English

A mod is a plugin that changes how Claude Code looks and behaves. This repository holds three, and works as a plugin marketplace, so you can install any of them by name.

| Mod | What it does | Where it works | | :- | :- | :- | | context-meter | Shows the git branch, how full the context window is, how much the last turn added, and how much of your plan limits you have used, in a band above the prompt | Terminal and the Desktop app's Code tab | | auto-pin | Pins a new session in the sidebar as soon as it starts | The Desktop app's Code tab | | context-handoff | Once the context window is 40% full, has Claude write hand-off notes, resets the context, and continues from the notes | Terminal and the Desktop app's Code tab |

Requirements

  • Terminal: Claude Code v2.1.287 or later. Check with claude --version.
  • Desktop app: a version that includes Claude Code v2.1.286 or later. In a local session in the Code tab, enter /status and read the Claude Code row.

Install

A mod is code that runs with your permissions: it can read and write your files, start processes, and make network requests. Read the source before you install, or list what each mod does.

Add this repository as a marketplace, then install the mods you want:

claude plugin marketplace add ulttla/claude-mod-repo
claude plugin install context-meter@claude-mod-repo
claude plugin install auto-pin@claude-mod-repo
claude plugin install context-handoff@claude-mod-repo

In a Claude Code session the same commands are /plugin marketplace add ulttla/claude-mod-repo and /plugin install context-meter@claude-mod-repo.

A mod loads the next time you start a session. In a session that is already open, run /reload-plugins.

Try one without installing

Clone the repository and load a mod's folder for one terminal session:

git clone https://github.com/ulttla/claude-mod-repo.git
claude --plugin-dir ./claude-mod-repo/context-meter

Check what a mod does first

With the repository cloned, this lists the events a mod handles and what it asks Claude Code to do, without running it:

claude plugin validate ./claude-mod-repo/context-meter

context-meter

Draws one line above the prompt:

main   Ctx 12% (119k/1M)   Last +119k   5H 1% ↻17:30   1W 25% ↻Wed 11:00

| Segment | Meaning | | :- | :- | | main | The current git branch. Left out when the session's folder is not in a git repository. On a detached HEAD, the short commit hash | | Ctx 12% (119k/1M) | Context window: percent full, tokens used, window size | | Last +119k | Tokens the last finished turn added. A negative number means the turn compacted the conversation | | 5H 1% ↻17:30 | Five-hour plan limit: percent used, and when it resets | | 1W 25% ↻Wed 11:00 | Weekly plan limit: percent used, and when it resets |

  • A segment turns yellow at 70% and bold red at 90%.
  • Reset times show only when the band is at least 100 columns wide.
  • Plan limits show only on a subscription plan. On an API key the band shows the other segments alone.
  • /context-meter prints the same line as text, for places that don't draw the band, such as the VS Code extension.
Options

Each segment can be turned on or off.

| Option | Default | What it shows | | :- | :- | :- | | showBranch | on | The current git branch | | showDirty | off | A * after the branch name when the working tree has uncommitted changes, as in main*. Runs git status on each turn | | showModel | off | The session's model | | showContext | on | Ctx, the context window | | showLastTurn | on | Last, what the last turn added | | showFiveHour | on | 5H, the five-hour plan limit | | showWeekly | on | 1W, the weekly plan limit | | showOtherLimits | on | Any other limit your account reports, such as a gateway's spend limit (Spend) | | showResetTimes | on | When each limit resets | | showCost | off | What the session has cost in US dollars, as in $1.24. On a subscription plan this is an estimate at API prices, not a charge |

To change them in a terminal session, run /plugin configure context-meter@claude-mod-repo. From your shell, pass each one when you install:

claude plugin install context-meter@claude-mod-repo --config showModel=true --config showCost=true

A change takes effect in the next session, or after /reload-plugins.

auto-pin

When you start a new session in the Desktop app's Code tab, the mod pins it in the sidebar. You unpin it yourself when the work is done.

  • A session you reopen is left as it is, so one you unpinned stays unpinned.
  • /auto-pin pins the current session on request, and says why when it can't.
  • Outside the Desktop app there is no sidebar, and the mod does nothing.
  • Sessions the app starts on its own, such as scheduled tasks, may be pinned too.

context-handoff

A long session gets worse as its context fills up. This mod hands the work over to a fresh context before that happens, without you opening a new session.

After each turn it reads how full the context window is. Past the threshold (40% by default), it:

  1. Submits a prompt that has Claude record a hand-off. When the project has a session-close skill, Claude is told to run it; otherwise Claude follows the session-close procedure in the project's CLAUDE.md, or updates PROGRESS.md, or writes HANDOFF.md at the project root.
  2. When that turn ends, resets the context. By default it compacts the conversation down to where the notes are and the next step (the same as /compact with instructions), so the process, model and settings stay as they were. In the Desktop app the session has no compaction call of its own, so the mod runs /compact with those instructions instead, as if you had typed it. With reset set to clear, it instead asks the Desktop app to clear the conversation (the same as /clear): the session keeps its row in the sidebar, the old conversation stays under Resume previous session, but the app starts a new Claude Code process only at your next message.
  3. Submits a prompt that has Claude read the notes and continue from the recorded next step.

Each step shows a toast. A message you had already queued runs first. If you interrupt the hand-off turn, or the reset fails, the mod gives up, says why, and tries again once the context has grown by another 5 points.

  • /handoff-now hands off right away, at any context size. /handoff-status shows where the hand-off stands and what the mod did last.
  • The prompts the mod submits are shown in the transcript, marked [context-handoff], and each step leaves a dim context-handoff: line there.
  • With the compaction through /compact (the Desktop app, or compaction set to command): the /compact line shows in the transcript, the resume follows once its compaction has run, and if nothing has been compacted within five minutes the mod gives up.
  • With the clear: the app is asked as the hand-off turn starts and clears as it ends; if it has not within eight seconds, the mod gives up. If a message you queued runs after the hand-off turn, the notes are recorded again. The resume is written to the mod's store, so the process the app starts next continues from the notes. The app starts that process only when you send your next message, however long after the clear: that message runs first, then the resume. Until then the conversation stays empty. A session started from another device cannot be cleared, so Remote Control is turned off for the clear and on again after the resume (pauseRemoteControl). Outside the Desktop app there is no app to clear, so the mod gives up after recording the notes.
Options

| Option | Default | What it does | | :- | :- | :- | | enabled | on | Hand off on its own past the threshold. Off, only /handoff-now hands off | | threshold | 40 | How full the context window is, in percent, before the hand-off starts | | retriggerStep | 5 | After an interrupted or failed hand-off, try again once the context has grown this many points | | reset | compact | How the context is reset: compact (in place, process and settings kept, no input needed) or clear (the Desktop app's clear: a fresh conversation, continued only after your next message) | | compaction | auto | How the compaction is made: auto (the engine's compaction call, or /compact where the session has none, as in the Desktop app), call (the call alone) or command (/compact alone, queued as if typed) | | closeCommand | session-close | The project skill Claude is told to run to record the hand-off, when the project has it | | closePrompt | empty | Replaces the built-in hand-off prompt and the close skill. {percent} and {threshold} are filled in | | resumePrompt | empty | Replaces the built-in prompt submitted after the reset | | pauseRemoteControl | on | With the clear: turn Remote Control off for it, and on again after the resume. Off, such a session is not cleared |

To change them in a terminal session, run /plugin configure context-handoff@claude-mod-repo, or pass them when you install:

claude plugin install context-handoff@claude-mod-repo --config threshold=50

Uninstall

claude plugin uninstall context-meter@claude-mod-repo
claude plugin uninstall auto-pin@claude-mod-repo
claude plugin uninstall context-handoff@claude-mod-repo

License

MIT. Shared as is, without support.


한국어

mod는 Claude Code의 모양과 동작을 바꾸는 플러그인입니다. 이 리포에는 mod 세 개가 들어 있고, 리포 자체가 플러그인 마켓플레이스 역할을 하므로 이름으로 골라 설치할 수 있습니다.

| Mod | 하는 일 | 동작하는 곳 | | :- | :- | :- | | context-meter | git 브랜치, 컨텍스트 창이 얼마나 찼는지, 직전 턴이 얼마나 늘렸는지, 플랜 한도를 얼마나 썼는지를 입력창 위 한 줄로 표시 | 터미널, 데스크톱 앱 Code 탭 | | auto-pin | 새 세션이 시작되면 사이드바에 바로 고정 | 데스크톱 앱 Code 탭 | | context-handoff | 컨텍스트 창이 40% 차면 Claude가 인계 기록을 쓰게 하고, 컨텍스트를 초기화한 뒤, 기록을 읽어 이어감 | 터미널, 데스크톱 앱 Code 탭 |

요구 사항

  • 터미널: Claude Code v2.1.287 이상. claude --version으로 확인합니다.
  • 데스크톱 앱: Claude Code v2.1.286 이상이 포함된 버전. Code 탭의 로컬 세션에서 /status를 입력하고 Claude Code 행을 봅니다.

설치

mod는 사용자 권한으로 실행되는 코드입니다. 파일을 읽고 쓰고, 프로세스를 실행하고, 네트워크 요청을 보낼 수 있습니다. 설치하기 전에 소스를 읽거나 mod가 하는 일을 먼저 확인하세요.

이 리포를 마켓플레이스로 추가한 뒤 원하는 mod를 설치합니다.

claude plugin marketplace add ulttla/claude-mod-repo
claude plugin install context-meter@claude-mod-repo
claude plugin install auto-pin@claude-mod-repo
claude plugin install context-handoff@claude-mod-repo

Claude Code 세션 안에서는 같은 명령을 /plugin marketplace add ulttla/claude-mod-repo, /plugin install context-meter@claude-mod-repo로 입력합니다.

mod는 다음에 세션을 시작할 때 로드됩니다. 이미 열려 있는 세션에서는 /reload-plugins를 실행합니다.

설치하지 않고 써 보기

리포를 클론한 뒤 터미널 세션 하나에만 mod 폴더를 로드합니다.

git clone https://github.com/ulttla/claude-mod-repo.git
claude --plugin-dir ./claude-mod-repo/context-meter

mod가 하는 일 먼저 확인하기

리포를 클론한 상태에서 아래 명령을 실행하면, mod를 실행하지 않고 어떤 이벤트를 처리하고 Claude Code에 무엇을 요청하는지 나열합니다.

claude plugin validate ./claude-mod-repo/context-meter

context-meter

입력창 위에 한 줄을 그립니다.

main   Ctx 12% (119k/1M)   Last +119k   5H 1% ↻17:30   1W 25% ↻Wed 11:00

| 항목 | 의미 | | :- | :- | | main | 현재 git 브랜치. 세션 폴더가 git 리포가 아니면 생략됩니다. detached HEAD에서는 짧은 커밋 해시 | | Ctx 12% (119k/1M) | 컨텍스트 창: 찬 비율, 사용한 토큰, 창 크기 | | Last +119k | 직전에 끝난 턴이 늘린 토큰. 음수면 그 턴에서 대화가 압축된 것 | | 5H 1% ↻17:30 | 5시간 플랜 한도: 사용률과 초기화 시각 | | 1W 25% ↻Wed 11:00 | 주간 플랜 한도: 사용률과 초기화 시각 |

  • 각 항목은 70%부터 노란색, 90%부터 굵은 빨간색으로 바뀝니다.
  • 초기화 시각은 밴드 폭이 100칸 이상일 때만 표시됩니다.
  • 플랜 한도는 구독 플랜에서만 표시됩니다. API 키로 쓰면 나머지 항목만 나옵니다.
  • /context-meter는 같은 내용을 텍스트로 출력합니다. VS Code 확장처럼 밴드를 그리지 않는 곳에서 씁니다.
옵션

항목마다 켜고 끌 수 있습니다.

| 옵션 | 기본값 | 표시 내용 | | :- | :- | :- | | showBranch | 켜짐 | 현재 git 브랜치 | | showDirty | 꺼짐 | 커밋하지 않은 변경이 있으면 브랜치 이름 뒤에 * 표시 (예: main*). 턴마다 git status를 실행합니다 | | showModel | 꺼짐 | 세션의 모델 | | showContext | 켜짐 | Ctx, 컨텍스트 창 | | showLastTurn | 켜짐 | Last, 직전 턴이 늘린 양 | | showFiveHour | 켜짐 | 5H, 5시간 플랜 한도 | | showWeekly | 켜짐 | 1W, 주간 플랜 한도 | | showOtherLimits | 켜짐 | 계정이 보고하는 그 밖의 한도. 예: 게이트웨이의 지출 한도(Spend) | | showResetTimes | 켜짐 | 각 한도의 초기화 시각 | | showCost | 꺼짐 | 세션 비용(미국 달러, 예: $1.24). 구독 플랜에서는 실제 청구액이 아니라 API 가격 기준 추정치입니다 |

터미널 세션에서는 /plugin configure context-meter@claude-mod-repo로 바꿉니다. 셸에서는 설치할 때 하나씩 넘깁니다.

claude plugin install context-meter@claude-mod-repo --config showModel=true --config showCost=true

변경은 다음 세션부터, 또는 /reload-plugins 후에 적용됩니다.

auto-pin

데스크톱 앱 Code 탭에서 새 세션을 시작하면 사이드바에 고정합니다. 작업이 끝나면 직접 고정을 해제하면 됩니다.

  • 다시 연 세션은 건드리지 않으므로, 고정을 해제한 세션은 해제된 채로 남습니다.
  • /auto-pin은 현재 세션을 직접 고정하고, 고정하지 못하면 이유를 알려 줍니다.
  • 데스크톱 앱 밖에는 사이드바가 없으므로 아무 일도 하지 않습니다.
  • 예약 작업처럼 앱이 스스로 시작하는 세션도 고정될 수 있습니다.

context-handoff

긴 세션은 컨텍스트가 찰수록 품질이 떨어집니다. 이 mod는 그 전에 작업을 새 컨텍스트로 넘깁니다. 새 세션을 직접 열 필요가 없습니다.

턴이 끝날 때마다 컨텍스트 창이 얼마나 찼는지 읽고, 임계값(기본 40%)을 넘으면 다음을 차례로 합니다.

  1. Claude가 인계 기록을 쓰도록 프롬프트를 제출합니다. 프로젝트에 session-close 스킬이 있으면 그 스킬을 실행하라고 지시하고, 없으면 프로젝트 CLAUDE.md의 세션 종료 절차를 따르거나, PROGRESS.md를 갱신하거나, 프로젝트 루트에 HANDOFF.md를 씁니다.
  2. 그 턴이 끝나면 컨텍스트를 초기화합니다. 기본은 대화를 "기록 위치와 다음 단계"만 남기고 압축하는 것(/compact에 지시문을 붙인 것과 같음)이라 프로세스·모델·설정이 그대로 유지됩니다. 데스크톱 앱의 세션에는 압축 호출이 없으므로, 거기서는 직접 입력한 것처럼 같은 지시문으로 /compact를 실행합니다. reset을 clear로 두면 대신 데스크톱 앱에 대화를 비워 달라고 요청합니다(/clear와 같음). 세션은 사이드바의 같은 행에 남고 이전 대화는 Resume previous session으로 되돌릴 수 있지만, 앱은 다음 메시지를 보낼 때에야 새 Claude Code 프로세스를 띄웁니다.
  3. Claude가 기록을 읽고 기록된 다음 시작점부터 이어가도록 프롬프트를 제출합니다.

단계마다 토스트로 알립니다. 이미 대기 중이던 메시지가 있으면 그것이 먼저 실행됩니다. 인계 턴을 중단하거나 초기화에 실패하면 포기하고 이유를 알린 뒤, 컨텍스트가 5포인트 더 차면 다시 시도합니다.

  • /handoff-now는 컨텍스트 크기와 상관없이 바로 인계합니다. /handoff-status는 인계가 어느 단계인지와 마지막으로 한 일을 보여 줍니다.
  • mod가 제출하는 프롬프트는 [context-handoff] 표시와 함께 대화에 보이고, 단계마다 흐릿한 context-handoff: 줄이 대화에 남습니다.
  • /compact로 압축할 때(데스크톱 앱, 또는 compaction을 command로 둔 경우): /compact 줄이 대화에 보이고, 그 압축이 끝나면 재개합니다. 5분 안에 압축되지 않으면 포기합니다.
  • clear일 때: 인계 턴이 시작될 때 앱에 요청하고 턴이 끝날 때 비워집니다. 8초 안에 비워지지 않으면 포기합니다. 대기 중이던 메시지가 인계 턴 뒤에 실행되면 기록을 다시 씁니다. 재개 지시는 mod 저장소에 적어 두므로 앱이 다음에 띄우는 프로세스가 기록을 읽고 이어갑니다. 앱은 비운 뒤 아무리 시간이 지나도 다음 메시지를 보낼 때에야 그 프로세스를 띄우며, 그 메시지가 먼저 실행된 뒤 재개합니다. 그때까지 대화는 비어 있습니다. 다른 기기에서 시작한 세션은 비울 수 없으므로 비우는 동안 Remote Control을 끄고 재개 후 다시 켭니다(pauseRemoteControl). 데스크톱 앱 밖에는 비워 줄 앱이 없으므로 기록만 쓴 뒤 포기합니다.
옵션

| 옵션 | 기본값 | 하는 일 | | :- | :- | :- | | enabled | 켜짐 | 임계값을 넘으면 자동으로 인계. 끄면 /handoff-now로만 인계 | | threshold | 40 | 인계를 시작하는 컨텍스트 창 사용률(%) | | retriggerStep | 5 | 중단되거나 실패한 인계를 컨텍스트가 몇 포인트 더 찼을 때 다시 시도할지 | | reset | compact | 컨텍스트 초기화 방식: compact(제자리 압축, 프로세스·설정 유지, 입력 불필요) 또는 clear(데스크톱 앱의 비우기: 새 대화, 다음 메시지를 보내야 이어감) | | compaction | auto | 압축 방법: auto(엔진의 압축 호출, 데스크톱 앱처럼 호출이 없는 세션에서는 /compact), call(호출만), command(/compact만, 입력한 것처럼 큐에 넣음) | | closeCommand | session-close | 프로젝트에 있을 때 인계 기록용으로 실행하라고 지시할 스킬 | | closePrompt | 비어 있음 | 내장 인계 프롬프트와 스킬 지시를 대체. {percent}, {threshold}가 채워짐 | | resumePrompt | 비어 있음 | 초기화 뒤 제출하는 내장 프롬프트를 대체 | | pauseRemoteControl | 켜짐 | clear일 때 비우는 동안 Remote Control을 끄고 재개 후 다시 켬. 끄면 그런 세션은 비우지 않음 |

터미널 세션에서는 /plugin configure context-handoff@claude-mod-repo로 바꾸거나, 설치할 때 넘깁니다.

claude plugin install context-handoff@claude-mod-repo --config threshold=50

제거

claude plugin uninstall context-meter@claude-mod-repo
claude plugin uninstall auto-pin@claude-mod-repo
claude plugin uninstall context-handoff@claude-mod-repo

라이선스

MIT. 지원 없이 있는 그대로 공유합니다.

Source 1 files
hooks/register.js 208 lines
1// The latest measurement, shared by the hooks below
2let usage = null
3// Context tokens when the current turn began, and whether that turn is still running
4let turnBase = null
5let turnOpen = false
6// Context tokens the last finished turn added
7let lastDelta = null
8// The branch the session's directory is on, null outside a git repository
9let branch = null
10// The session's model, as /model shows it
11let model = null
12
13const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
14
15// Which segments to show, from the options plugin.json declares
16function shownSegments(options) {
17  return {
18    branch: options.showBranch !== false,
19    dirty: options.showDirty === true,
20    model: options.showModel === true,
21    context: options.showContext !== false,
22    lastTurn: options.showLastTurn !== false,
23    fiveHour: options.showFiveHour !== false,
24    weekly: options.showWeekly !== false,
25    otherLimits: options.showOtherLimits !== false,
26    resets: options.showResetTimes !== false,
27    cost: options.showCost === true,
28  }
29}
30
31// 12345 -> "12.3k", 150236 -> "150k", 1000000 -> "1M"
32function formatTokens(n) {
33  const abs = Math.abs(n)
34  if (abs < 1000) return String(n)
35  const [value, unit] = abs < 999500 ? [n / 1000, 'k'] : [n / 1000000, 'M']
36  return (Math.abs(value) < 100 ? Number(value.toFixed(1)) : Math.round(value)) + unit
37}
38
39// A negative delta means the turn compacted the conversation
40function formatDelta(n) {
41  if (n === 0) return '±0'
42  return (n > 0 ? '+' : '−') + formatTokens(Math.abs(n))
43}
44
45function contextPercent(context) {
46  if (typeof context.percent === 'number') return context.percent
47  return context.window > 0 ? (context.tokens / context.window) * 100 : 0
48}
49
50// The plan limit kinds Claude Code reports: the label drawn, and the segment that shows it
51const LIMITS = {
52  five_hour: { label: '5H', segment: 'fiveHour' },
53  seven_day: { label: '1W', segment: 'weekly' },
54  spend_limit: { label: 'Spend', segment: 'otherLimits' },
55}
56
57// One plan limit as the band draws it. An unknown kind is shown as it is, with the other limits.
58function limitOf(raw) {
59  // Round to the minute, so a reset at 12:29:59 reads 12:30
60  const ms = raw.resetsAt ? Math.round(new Date(raw.resetsAt).getTime() / 60000) * 60000 : NaN
61  const { label, segment } = LIMITS[raw.kind] ?? { label: String(raw.kind), segment: 'otherLimits' }
62  return {
63    label,
64    segment,
65    percent: raw.percentUsed,
66    resetsAt: Number.isNaN(ms) ? null : new Date(ms),
67  }
68}
69
70// "12:30" for a reset within a day, "Wed 11:00" for one further off
71function formatReset(date, now) {
72  const time = String(date.getHours()).padStart(2, '0') + ':' + String(date.getMinutes()).padStart(2, '0')
73  return date.getTime() - now < 24 * 60 * 60 * 1000 ? time : DAYS[date.getDay()] + ' ' + time
74}
75
76// The segments in the order they are drawn. A segment with a percent is colored by it.
77function segments(shown, hasRoom, now) {
78  const { context, rateLimits, cost } = usage
79  const parts = []
80  if (shown.branch && branch) parts.push({ text: branch })
81  if (shown.model && model) parts.push({ text: model })
82  if (shown.context) {
83    // The fill is absent until the first response reports one
84    const isFilled = typeof context.tokens === 'number'
85    const percent = isFilled ? contextPercent(context) : undefined
86    const fill = isFilled ? `${Math.round(percent)}% (${formatTokens(context.tokens)}` : '– (–'
87    parts.push({ text: `Ctx ${fill}/${formatTokens(context.window)})`, percent })
88  }
89  if (shown.lastTurn) parts.push({ text: 'Last ' + (lastDelta === null ? '–' : formatDelta(lastDelta)) })
90  for (const limit of rateLimits.map(limitOf)) {
91    if (!shown[limit.segment]) continue
92    const reset = shown.resets && hasRoom && limit.resetsAt ? ' ↻' + formatReset(limit.resetsAt, now) : ''
93    parts.push({ text: `${limit.label} ${Math.round(limit.percent)}%${reset}`, percent: limit.percent })
94  }
95  // Absent where Claude Code keeps no cost ledger
96  if (shown.cost && cost) parts.push({ text: '$' + cost.usd.toFixed(2) })
97  return parts
98}
99
100// Text props for how full something is: plain, then yellow, then bold red
101function tone(percent) {
102  if (typeof percent !== 'number' || percent < 70) return {}
103  return percent < 90 ? { color: 'yellow' } : { color: 'red', bold: true }
104}
105
106// Run git in the session's directory. Resolves to what it printed, or null when it failed.
107async function git($, ...args) {
108  try {
109    const { exitCode, stdout } = await $.process.run(['git', ...args], { timeoutMs: 5000 })
110    return exitCode === 0 ? stdout.trim() : null
111  } catch {
112    return null
113  }
114}
115
116// Read the branch again and ask for a redraw. A branch with uncommitted changes gets a `*`.
117async function readBranch($, shown) {
118  if (!shown.branch) return
119  let name = await git($, 'branch', '--show-current')
120  // A detached HEAD has no branch name, so show its commit
121  if (name === '') name = await git($, 'rev-parse', '--short', 'HEAD')
122  const isDirty = Boolean(name) && shown.dirty && Boolean(await git($, 'status', '--porcelain'))
123  branch = name ? name + (isDirty ? '*' : '') : null
124  $.ui.invalidate('ui.render')
125}
126
127// Read the figures again and ask for a redraw. A failed read keeps the last figures.
128async function measure($, shown) {
129  try {
130    usage = await $.session.usage()
131    if (shown.model) model = await $.session.model()
132  } catch {
133    return
134  }
135  // Once the turn has ended, what it added is the difference from where it began
136  const { tokens } = usage.context
137  if (!turnOpen && turnBase !== null && typeof tokens === 'number') lastDelta = tokens - turnBase
138  $.ui.invalidate('ui.render')
139}
140
141export function register(on, options) {
142  const shown = shownSegments(options)
143
144  // Runs before your first prompt, and again after a reload
145  on('session.start', async ($, e, next) => {
146    await $.command.register({
147      name: 'context-meter',
148      description: 'Show context usage, what the last turn added, and plan limit usage',
149      immediate: true,
150    })
151    await readBranch($, shown)
152    await measure($, shown)
153    return next(e)
154  })
155
156  on('turn.start', async ($, e, next) => {
157    // You may have switched branches since the last turn
158    await readBranch($, shown)
159    await measure($, shown)
160    // Before the first response nothing is in the window yet
161    if (usage) turnBase = usage.context.tokens ?? 0
162    turnOpen = true
163    return next(e)
164  })
165
166  on('turn.complete', async ($, e, next) => {
167    turnOpen = false
168    // The turn may have switched branches or changed files
169    await readBranch($, shown)
170    await measure($, shown)
171    return next(e)
172  })
173
174  // Fires after each turn, and when a plan limit's percent used changes
175  on('session.measure', async ($, e, next) => {
176    await measure($, shown)
177    return next(e)
178  })
179
180  // The same line as text, for apps that don't draw the band
181  on('command.run', { command: 'context-meter' }, async ($) => {
182    await readBranch($, shown)
183    await measure($, shown)
184    if (!usage) return { text: 'No usage reading yet' }
185    const text = segments(shown, true, Date.now())
186      .map((part) => part.text)
187      .join(' · ')
188    return { text: text || 'Every segment is turned off' }
189  })
190
191  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
192    // Keep what the mods after this one draw in the band
193    const theirs = await next(e)
194    // Stay out of the way of a survey, and draw nothing before the first reading
195    if (!usage || e.props.hasSurvey) return theirs
196    // The reset times fit only in a wide band
197    const parts = segments(shown, (e.props.bodyColumns ?? 0) >= 100, Date.now())
198    if (parts.length === 0) return theirs
199    const { Box, Text } = $.ui.resolve(e)
200    const row = Box({
201      flexDirection: 'row',
202      columnGap: 3,
203      children: parts.map((part) => Text({ ...tone(part.percent), children: [part.text] })),
204    })
205    return theirs ? Box({ flexDirection: 'column', children: [row, theirs] }) : row
206  })
207}
208