SLOPSHOPPER

kontext-waechter

Zeigt den Füllstand des Kontexts und sagt, wann eine Zusammenfassung und eine neue Session Token spart.

newbandcommandtoaststatusmodel
v0.1.4MITupdated 2026-10-24KaelanTesseract/kontext-waechter
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · kontext-waechter
› 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 › /waechter ⎿ kontext-waechter: Kontext-Wächter ausgeblendet. Stand: 49% (97k von 200k Tokens). Kontext frei, weiterarbeiten. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ kontext-waechter: Kontext 49% █████░░░░░
README

Context Watcher (Kontext-Wächter)

Deutsche Version

A mod for Claude Code that shows how full your session's context is, tells you when it is worth switching to a fresh session, and writes a handover note for you with one click. Fewer wasted tokens, better answers in long sessions.

/plugin install kontext-waechter --marketplace KaelanTesseract/kontext-waechter

The bar above the prompt box

Why

With every reply, Claude sends the whole conversation again. The longer a session runs, the more is carried along on each message: it gets more expensive, and answers tend to get less precise. Starting fresh at the right moment, with a clean summary, saves money and keeps quality up. The Context Watcher does the watching for you.

What it does

Context fill at a glance. A bar above the prompt box shows the percentage, the tokens in use and the cost so far. It updates after every reply and also appears in the status line.

A recommendation for when to stop.

Fill levelAdvice
below 50 %Keep working
from 50 %Start fresh when you change topic
from 70 %A summary and a new session pay off now
from 85 %Summarize and start fresh now

A short notice also appears when you cross 70 % and 85 %.

Handover in one click. The "Übergabe" button (handover) has Claude write a note for the next session: goal, current state, decisions made, important files, next steps, known pitfalls and how you like to work. It is saved in your project folder as .claude/uebergabe/LETZTE.md, plus a dated copy.

New session opened for you (desktop app). The second button, "+ neue Session", also asks Claude to open a fresh session in the same folder that reads the handover and carries on. The mod cannot open sessions itself, so it asks Claude to do it; this works in the desktop app only.

Seamless continuation. In the new session, type /weiter. The handover is loaded, Claude summarizes where you stand and carries on with the first next step. If an unread handover is waiting at startup, the mod tells you.

Subagents counted separately. Subagents have their own context and do not fill the main session's. They are not part of the bar, but their runs and tokens are shown next to it.

Commands

CommandEffect
/uebergabeCreates the handover, same as the button
/weiterLoads the last handover into the new session
/waechterShows or hides the bar

Install

In a terminal session:

/plugin install kontext-waechter --marketplace KaelanTesseract/kontext-waechter

From a shell:

claude plugin install kontext-waechter --marketplace KaelanTesseract/kontext-waechter

In the desktop app, /plugin does not run. Click + next to the prompt box, then Plugins, Add marketplace with KaelanTesseract/kontext-waechter, then Add plugin.

To update: claude plugin update kontext-waechter.

New session or /compact?

/compact replaces the conversation inside the running session with a summary that Claude chooses itself. It is quick, but details can get lost unnoticed. A new session with a handover starts empty and only knows what you hand over. You can read and edit the handover first.

Rule of thumb: same topic and 50 to 70 % full, /compact is enough. From 70 %, or for a new topic, the handover is the better choice.

Customize

The thresholds (50, 70, 85 percent) are at the top of hooks/register.tsx. The text that asks for the handover is there too (it is written in German; change it to your language if you like).

Notes

  • The "create handover" button only works after at least one reply has run in the session.
  • Handover files live in your project folder. To keep them out of Git, add .claude/uebergabe/ to your .gitignore.
  • The dollar amount comes from Claude Code itself. Whether it includes subagent costs depends on how Claude Code counts.
  • The fill level is the value Claude Code reports after the last reply. It does not change between replies.
  • The interface text is in German.

License

MIT, see LICENSE.

Source 2 files
hooks/register.tsx 294 lines
1import type { Register } from 'claude-code'
2
3import type { Stand } from '../types'
4
5const stand = { plugin: 'kontext-waechter', key: 'stand' } as const
6const isHidden = { plugin: 'kontext-waechter', key: 'isHidden' } as const
7const isBusy = { plugin: 'kontext-waechter', key: 'isBusy' } as const
8
9const ORDNER = '.claude/uebergabe'
10const LETZTE = `${ORDNER}/LETZTE.md`
11const GELESEN = `${ORDNER}/gelesen.txt`
12
13// Schwellen in Prozent des Kontextfensters. Jede Antwort schickt den ganzen
14// Verlauf erneut mit, darum wird jede weitere Runde teurer.
15const BEOBACHTEN = 50
16const SINNVOLL = 70
17const DRINGEND = 85
18
19const UEBERGABE_PROMPT = `Schreibe jetzt eine Übergabe für eine neue Session, die dieses Gespräch nicht kennt und nahtlos weiterarbeiten soll. Antworte nur mit dem Dokument, ohne Einleitung, auf Deutsch, in Markdown, so knapp wie möglich, aber vollständig:
20
21# Übergabe
22## Ziel
23Was soll am Ende erreicht sein, und warum.
24## Stand
25Was ist fertig, was läuft gerade, was ist noch offen.
26## Entscheidungen
27Getroffene Entscheidungen samt Begründung, damit sie nicht neu diskutiert werden.
28## Wichtige Orte
29Dateien, Ordner, Befehle, Adressen, die gebraucht werden (mit vollständigen Pfaden).
30## Nächste Schritte
31Geordnete Liste, der erste Punkt ist sofort ausführbar.
32## Stolpersteine
33Fehlschläge, Sackgassen und Eigenheiten, die man sonst wieder erleben würde.
34## Vorlieben des Nutzers
35Wie der Nutzer arbeiten möchte, soweit im Gespräch sichtbar.
36
37Keine Vermutungen als Tatsachen darstellen. Nichts aufnehmen, was sich leicht aus dem Code oder Git-Verlauf ablesen lässt.`
38
39type Stufe = { text: string; color: 'success' | 'warning' | 'error'; rank: number }
40
41const stufe = (percent: number): Stufe => {
42  if (percent >= DRINGEND) {
43    return { rank: 3, color: 'error', text: 'Jetzt zusammenfassen lassen und neue Session starten' }
44  }
45  if (percent >= SINNVOLL) {
46    return { rank: 2, color: 'warning', text: 'Zusammenfassung und neue Session lohnen sich jetzt' }
47  }
48  if (percent >= BEOBACHTEN) {
49    return { rank: 1, color: 'warning', text: 'Kontext füllt sich, bei einem Themenwechsel neu starten' }
50  }
51  return { rank: 0, color: 'success', text: 'Kontext frei, weiterarbeiten' }
52}
53
54const balken = (percent: number) => {
55  const full = Math.max(0, Math.min(10, Math.round(percent / 10)))
56  return '█'.repeat(full) + '░'.repeat(10 - full)
57}
58
59const k = (n: number) => (n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`)
60
61const stempel = (text: string) => {
62  const m = /<!-- erstellt: (\d+) -->/.exec(text)
63  return m === null ? '' : m[1]
64}
65
66const dateiname = (ms: number) =>
67  new Date(ms).toISOString().slice(0, 16).replace('T', '_').replace(':', '-')
68
69export const register: Register = on => {
70  let turns = 0
71  let lastRank = 0
72  let agentRuns = 0
73  let agentTokens = 0
74
75  on('session.start', async ($, e, next) => {
76    await $.command.register({
77      name: 'waechter',
78      description: 'Kontext-Wächter: Anzeige ein-/ausblenden',
79    })
80    await $.command.register({
81      name: 'uebergabe',
82      description: 'Zusammenfassung für die nächste Session erstellen und speichern',
83    })
84    await $.command.register({
85      name: 'weiter',
86      description: 'Letzte Übergabe laden und damit weiterarbeiten',
87    })
88
89    try {
90      const letzte = await $.fs.read(LETZTE)
91      let gelesen = ''
92      try {
93        gelesen = await $.fs.read(GELESEN)
94      } catch {
95        gelesen = ''
96      }
97      if (stempel(letzte) !== '' && stempel(letzte) !== gelesen.trim()) {
98        $.ui.toast('Übergabe der letzten Session gefunden. Mit /weiter laden.')
99      }
100    } catch {
101      // keine Übergabe vorhanden
102    }
103
104    return next(e)
105  })
106
107  on('turn.complete', async ($, e, next) => {
108    if (e.agentId === undefined) {
109      turns += 1
110      const u = await $.session.usage()
111      const percent = u.context.percent ?? 0
112      const s: Stand = {
113        percent,
114        tokens: u.context.tokens ?? 0,
115        window: u.context.window,
116        usd: u.cost?.usd ?? null,
117        turns,
118        agentRuns,
119        agentTokens,
120      }
121      await $.state.set(stand, s)
122
123      const st = stufe(percent)
124      $.ui.status(`Kontext ${percent}% ${balken(percent)}`)
125      if (st.rank > lastRank && st.rank >= 2) {
126        $.ui.toast(`Kontext ${percent}% voll: ${st.text}.`)
127      }
128      lastRank = st.rank
129    } else if (e.usage !== undefined) {
130      // Subagenten haben ein eigenes Fenster: sie zählen nur in den Verbrauch,
131      // nicht in den Füllstand der Hauptsession.
132      agentRuns += 1
133      agentTokens +=
134        e.usage.input_tokens + e.usage.output_tokens + e.usage.cache_creation_input_tokens
135      const { value: s = null } = await $.state.get(stand)
136      if (s !== null) {
137        await $.state.set(stand, { ...s, agentRuns, agentTokens })
138      }
139    }
140    return next(e)
141  })
142
143  on('command.run', { command: 'waechter' }, async $ => {
144    const { value: hidden = false } = await $.state.get(isHidden)
145    await $.state.set(isHidden, !hidden)
146
147    // Gleich messen: in einer Session, in der die Mod eben erst geladen wurde,
148    // gibt es sonst bis zur nächsten Antwort keinen Wert und keine Leiste.
149    const u = await $.session.usage()
150    const percent = u.context.percent ?? 0
151    const s: Stand = {
152      percent,
153      tokens: u.context.tokens ?? 0,
154      window: u.context.window,
155      usd: u.cost?.usd ?? null,
156      turns,
157      agentRuns,
158      agentTokens,
159    }
160    await $.state.set(stand, s)
161    $.ui.status(`Kontext ${percent}% ${balken(percent)}`)
162    lastRank = stufe(percent).rank
163
164    const st = stufe(percent)
165    const stand_ = s.tokens === 0
166      ? 'Noch keine Antwort in dieser Session gemessen, die Leiste füllt sich nach der nächsten Antwort.'
167      : `Stand: ${percent}% (${k(s.tokens)} von ${k(s.window)} Tokens). ${st.text}.`
168
169    return { text: `Kontext-Wächter ${hidden ? 'eingeblendet' : 'ausgeblendet'}. ${stand_}` }
170  })
171
172  on('command.run', { command: 'uebergabe' }, async $ => {
173    const { value: busy = false } = await $.state.get(isBusy)
174    if (busy) {
175      return { text: 'Die Übergabe wird gerade erstellt.' }
176    }
177    await $.state.set(isBusy, true)
178    const r = await $.model.fork({ prompt: UEBERGABE_PROMPT })
179    await $.state.set(isBusy, false)
180
181    if (!r.isAnswered) {
182      return { text: `Übergabe nicht möglich (${r.reason}). Nach der ersten Antwort noch einmal versuchen.` }
183    }
184
185    const jetzt = await $.clock.now()
186    const d = dateiname(jetzt)
187    const doc = `<!-- erstellt: ${jetzt} -->\n${r.text.trim()}\n`
188    await $.fs.write(`${ORDNER}/uebergabe-${d}.md`, doc)
189    await $.fs.write(LETZTE, doc)
190
191    return {
192      text:
193        `Übergabe gespeichert in ${LETZTE} (Kopie: uebergabe-${d}.md). ` +
194        'In der neuen Session einfach /weiter eingeben.',
195    }
196  })
197
198  on('command.run', { command: 'weiter' }, async $ => {
199    let doc = ''
200    try {
201      doc = await $.fs.read(LETZTE)
202    } catch {
203      return { text: `Keine Übergabe gefunden (${LETZTE}). Erst /uebergabe in der alten Session ausführen.` }
204    }
205    await $.fs.write(GELESEN, stempel(doc))
206    void $.prompt.submit({
207      text:
208        'Hier ist die Übergabe der vorherigen Session. Lies sie, fasse in zwei Sätzen zusammen, ' +
209        'wo wir stehen, und mach dann mit dem ersten nächsten Schritt weiter.\n\n' +
210        doc,
211    })
212
213    return { text: 'Übergabe wird geladen.' }
214  })
215
216  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
217    const { value: s = null } = await $.state.get(stand)
218    const { value: hidden = false } = await $.state.get(isHidden)
219    const { value: busy = false } = await $.state.get(isBusy)
220
221    if (e.props.hasSurvey || s === null || hidden) {
222      return next(e)
223    }
224
225    const { Box, Text, Button } = $.ui.resolve(e)
226    // Erstellt die Übergabe; mit neu=true bittet es danach Claude, eine neue
227    // Session zu öffnen (das kann nur die Desktop-App, die Mod selbst nicht).
228    const erstellen = async (neu: boolean) => {
229      await $.state.set(isBusy, true)
230      const r = await $.model.fork({ prompt: UEBERGABE_PROMPT })
231      await $.state.set(isBusy, false)
232      if (!r.isAnswered) {
233        $.ui.toast(`Übergabe nicht möglich (${r.reason}).`)
234        return
235      }
236      const jetzt = await $.clock.now()
237      const doc = `<!-- erstellt: ${jetzt} -->
238${r.text.trim()}
239`
240      await $.fs.write(`${ORDNER}/uebergabe-${dateiname(jetzt)}.md`, doc)
241      await $.fs.write(LETZTE, doc)
242
243      if (!neu) {
244        $.ui.toast('Übergabe gespeichert. In der neuen Session /weiter eingeben.')
245        return
246      }
247
248      const cwd = await $.session.cwd()
249      await $.fs.write(GELESEN, stempel(doc))
250      $.ui.toast('Übergabe gespeichert, neue Session wird geöffnet.')
251      void $.prompt.submit({
252        text:
253          `Die Übergabe liegt in ${cwd}/${LETZTE}. Öffne jetzt mit dem Werkzeug zum Starten einer Session ` +
254          `eine neue Session im Ordner ${cwd} (frischer Kontext, ohne diesen Verlauf), deren Auftrag lautet: ` +
255          `"Lies ${cwd}/${LETZTE}, fasse in zwei Sätzen zusammen, wo wir stehen, und mach mit dem ersten ` +
256          `nächsten Schritt weiter." Antworte hier danach nur mit einem Satz, dass die neue Session läuft. ` +
257          'Gibt es dieses Werkzeug nicht, sag das kurz und nenne den Befehl /weiter für die neue Session.',
258      })
259    }
260
261    const st = stufe(s.percent)
262    const cost = s.usd === null ? '' : ` · $${s.usd.toFixed(2)}`
263    const runs = s.agentRuns ?? 0
264    const agents = runs === 0 ? '' : ` · Agenten ${runs}×, ${k(s.agentTokens ?? 0)}`
265
266    return (
267      <Box>
268        <Text color={st.color}>
269          {balken(s.percent)} {s.percent}%{' '}
270        </Text>
271        <Text dimColor>
272          ({k(s.tokens)}/{k(s.window)}
273          {cost}
274          {agents}){' '}
275        </Text>
276        <Text color={st.color}>{st.text} </Text>
277        {busy ? (
278          <Text dimColor>Übergabe wird erstellt… </Text>
279        ) : (
280          <Box flexDirection="row" flexShrink={0}>
281            <Button key="uebergabe" label="Übergabe" onPress={() => erstellen(false)} />
282            <Text> </Text>
283            {e.surface === 'desktop' && (
284              <Button key="neu" label="+ neue Session" onPress={() => erstellen(true)} />
285            )}
286            <Text> </Text>
287          </Box>
288        )}
289        <Button key="hide" label="✕" onPress={() => $.state.set(isHidden, true)} />
290      </Box>
291    )
292  })
293}
294
types/index.d.ts 16 lines
1export type Stand = {
2  percent: number
3  tokens: number
4  window: number
5  usd: number | null
6  turns: number
7  agentRuns: number
8  agentTokens: number
9}
10
11declare module 'claude-code' {
12  interface PluginState {
13    'kontext-waechter': { stand: Stand | null; isHidden: boolean; isBusy: boolean }
14  }
15}
16