SLOPSHOPPER

cache-leiste

Leiste über dem Eingabefeld: Prompt-Cache-Restzeit, Cache-Hit-Rate, Kontext-Wetter und Compact-Knopf

newbandtoastprocesstimer
v0.1.0no licenseupdated 2026-10-11lechef312/claude-cache-leiste
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cache-leiste
› 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 Cache: – ░░░░░░░░░░░░░░░░░░░░ · Hit – · 🌧️ Kontext 49 % / Compact bei ~83 % [ Compact ] ◂ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Cache: – ░░░░░░░░░░░░░░░░░░░░ · Hit – · 🌧️ Kontext 49 % / Compact bei ~83 % [ Compact ] ◂
README

cache-leiste

Schmale Leiste über dem Eingabefeld von Claude Code, von links nach rechts:

  1. Cache-Restzeit in Minuten seit der letzten Antwort, mit der echten Laufzeit der Session (5 Min oder 1 h, gelesen aus dem Session-Protokoll), dazu ein Balken aus 20 Kästchen: grün, im letzten Viertel gelb, in den letzten 10 % rot, bei 0 „abgelaufen“.
  2. Hit-Rate der letzten Antwort: Anteil der Input-Tokens aus dem Cache.
  3. Wetter: ☀️ bis zur Hälfte der Auto-Compact-Schwelle, 🌧️ bis 80 % davon, ⛈️ darüber.
  4. Kontextfüllung in Prozent und die Auto-Compact-Schwelle (~83 %, solange sie nicht lesbar ist).
  5. Compact-Knopf, bei ⛈️ hervorgehoben. In der Desktop-App oder während Claude arbeitet wird /compact eingereiht und startet, sobald Claude fertig ist.
  6. Pfeil zum Zuklappen: dann nur Wetter, Cache-Restzeit und Kontext in Prozent.

Nach /compact oder dem Knopf steht im Chat eine Zeile mit den Tokens vorher und nachher und darunter die Zusammenfassung, mit der Claude weitermacht. Das sind Log-Zeilen, die das Modell nicht liest; lange Zusammenfassungen werden an Absätzen geteilt.

Installieren (für alle Projekte)

Im Terminal, direkt aus GitHub:

claude plugin marketplace add lechef312/claude-cache-leiste
claude plugin install cache-leiste@simons-mods --scope user

Oder in einer Claude-Code-Sitzung: /plugin install cache-leiste --marketplace lechef312/claude-cache-leiste.

Aus einem lokalen Ordner geht es genauso, mit dem Pfad statt lechef312/claude-cache-leiste. Danach Claude Code neu starten.

Cloud-Sitzungen (claude.ai/code)

Diese zwei Zeilen ins Setup-Skript der Cloud-Umgebung, dann lädt jede neue Cloud-Sitzung den Mod:

claude plugin marketplace add lechef312/claude-cache-leiste
claude plugin install cache-leiste@simons-mods --scope user

Prüfen

claude plugin validate .
claude plugin test .
Source 3 files
hooks/register.tsx 386 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer, UiPressArgument } from 'claude-code'
3
4import type { CacheTtl, LastResponse } from '../types'
5import {
6  BAR_CELLS,
7  FALLBACK_THRESHOLD,
8  cacheView,
9  formatTokens,
10  hitRate,
11  redrawOffsets,
12  splitParagraphs,
13  ttlFromTranscript,
14  weather,
15} from './logic'
16
17const last = atom({ plugin: 'cache-leiste', key: 'last' } as const, null)
18const ttl = atom({ plugin: 'cache-leiste', key: 'ttl' } as const, '5m')
19const isTtlRead = atom({ plugin: 'cache-leiste', key: 'isTtlRead' } as const, false)
20const context = atom({ plugin: 'cache-leiste', key: 'context' } as const, {
21  percent: null,
22  threshold: FALLBACK_THRESHOLD,
23  isThresholdRead: false,
24})
25const now = atom({ plugin: 'cache-leiste', key: 'now' } as const, 0)
26const isCollapsed = atom({ plugin: 'cache-leiste', key: 'isCollapsed' } as const, false)
27
28/** Wie viel vom Ende des Protokolls gelesen wird, um die Cache-Laufzeit zu finden. */
29const TRANSCRIPT_TAIL_BYTES = '2000000'
30
31// Modulvariablen: nach einem Neuladen leer, alles Gezeichnete liegt in $.state.
32let timers: Timer[] = []
33let isBusy = false
34/** Die letzte bekannte Kontextgröße; nach einem Compact bleibt sie bis zur nächsten Antwort stehen. */
35let contextTokens: number | undefined
36let pendingCompact: { before?: number; after?: number; fallback?: string; isLogged: boolean } | undefined
37let compactSummary: string | undefined
38/**
39 * Läuft gerade ein Compact, den der Knopf ausgelöst hat? Dessen Ereignisse
40 * laufen unter dem Aufruf des Mods und erreichen dessen Hooks nicht; der Knopf
41 * schreibt die Zusammenfassung dann selbst.
42 */
43let isOwnCompact = false
44
45async function setNow($: EngineInterface) {
46  const t = await $.clock.now()
47  await update($, now, () => t)
48}
49
50/** Zeichnet ab der letzten Antwort jede Minute neu, dazu beim Wechsel auf Gelb und Rot. */
51async function startTicking($: EngineInterface) {
52  for (const timer of timers) timer.cancel()
53  timers = []
54  await setNow($)
55
56  const response = await read($, last)
57  if (response === null) return
58  const elapsed = (await $.clock.now()) - response.at
59
60  for (const offset of redrawOffsets(await read($, ttl))) {
61    if (offset <= elapsed) continue
62    timers.push($.clock.after(offset - elapsed, () => void setNow($)))
63  }
64}
65
66async function setTtl($: EngineInterface, value: CacheTtl) {
67  const before = await read($, ttl)
68  await update($, ttl, () => value)
69  await update($, isTtlRead, () => true)
70  await $.store.set('ttl', value)
71  if (before !== value) await startTicking($)
72}
73
74async function readTtlFromTranscript($: EngineInterface, path: string) {
75  let text: string | undefined
76  try {
77    const tail = await $.process.run(['tail', '-c', TRANSCRIPT_TAIL_BYTES, path], { timeoutMs: 5000 })
78    if (tail.exitCode === 0) text = tail.stdout
79  } catch {
80    // Kein tail auf diesem Rechner: dann die Datei selbst, solange sie unter 4 MiB ist.
81  }
82  if (text === undefined) {
83    try {
84      text = await $.fs.read(path)
85    } catch {
86      return
87    }
88  }
89  const found = ttlFromTranscript(text)
90  if (found !== undefined) await setTtl($, found)
91}
92
93async function readThreshold($: EngineInterface) {
94  try {
95    const usage = await $.session.usage({ breakdown: 'summary' })
96    const breakdown = usage.context.breakdown
97    const tokens = breakdown?.isAutoCompactEnabled ? breakdown.autoCompactThreshold : undefined
98    if (tokens === undefined || usage.context.window <= 0) return
99    const threshold = Math.round((tokens / usage.context.window) * 100)
100    await update($, context, fill => ({ ...fill, threshold, isThresholdRead: true }))
101  } catch {
102    // Schwelle nicht lesbar: es bleibt bei 83 %.
103  }
104}
105
106async function setContext($: EngineInterface, percent: number | undefined, tokens: number | undefined) {
107  if (tokens !== undefined) contextTokens = tokens
108  await update($, context, fill => ({ ...fill, percent: percent ?? null }))
109}
110
111/** Nach einem Compact ist der alte Cache nichts mehr wert, bis die nächste Antwort kommt. */
112async function resetAfterCompact($: EngineInterface) {
113  await update($, last, () => null)
114  await update($, context, fill => ({ ...fill, percent: null }))
115  await startTicking($)
116}
117
118function writeSummary($: EngineInterface, before: number | undefined, after: number | undefined, summary: string | undefined) {
119  const from = before === undefined ? '?' : formatTokens(before)
120  const to = after === undefined ? '?' : formatTokens(after)
121  $.ui.log(`Compact: ${from} → ${to} Tokens. Damit macht Claude weiter:`)
122  if (summary === undefined || summary.trim() === '') {
123    $.ui.log('(keine Zusammenfassung erhalten)')
124    return
125  }
126  for (const chunk of splitParagraphs(summary)) $.ui.log(chunk)
127}
128
129/** Die Nachricht, mit der das Gespräch nach dem Compact beginnt: die Zusammenfassung. */
130function summaryOf(messages: readonly { role: string; text: string }[]): string | undefined {
131  return messages.find(message => message.role === 'user' && message.text.trim() !== '')?.text
132}
133
134/** Die Größe nach dem Compact, geschätzt, solange noch keine Antwort sie gemeldet hat. */
135async function estimateTokens($: EngineInterface): Promise<number | undefined> {
136  try {
137    const usage = await $.session.usage({ breakdown: 'summary' })
138    return usage.context.tokens ?? usage.context.breakdown?.totalTokens
139  } catch {
140    return undefined
141  }
142}
143
144function logCompact($: EngineInterface) {
145  if (pendingCompact === undefined || pendingCompact.isLogged) return
146  pendingCompact.isLogged = true
147  const { before, after, fallback } = pendingCompact
148  const summary = compactSummary ?? fallback
149  compactSummary = undefined
150  writeSummary($, before, after, summary)
151}
152
153async function compactNow($: EngineInterface) {
154  const before = contextTokens
155  const result = await $.session.compact()
156  if (result.skip !== undefined) {
157    $.ui.toast(`Compact übersprungen: ${result.skip}`)
158    return
159  }
160  await resetAfterCompact($)
161  writeSummary($, result.tokensBefore ?? before, result.tokensAfter, summaryOf(result.messages))
162}
163
164async function compactQueued($: EngineInterface) {
165  $.ui.toast('/compact ist eingereiht und startet, sobald Claude fertig ist.')
166  try {
167    const ran = await $.command.run({ command: 'compact' })
168    if (ran.text !== undefined && /error|fehler|failed|not enough/i.test(ran.text)) {
169      $.ui.toast(`/compact: ${ran.text}`)
170      return
171    }
172    const before = contextTokens
173    await resetAfterCompact($)
174    writeSummary($, before, await estimateTokens($), summaryOf(await $.session.messages()))
175  } catch {
176    $.ui.toast('/compact ließ sich nicht einreihen.')
177  }
178}
179
180async function compact($: EngineInterface, press: UiPressArgument, isWorkingAtDraw: boolean) {
181  if (isOwnCompact) {
182    $.ui.toast('Compact läuft schon.')
183    return
184  }
185  isOwnCompact = true
186  try {
187    // In der Desktop-App und während einer Runde geht der direkte Aufruf nicht: einreihen.
188    if (press.surface !== 'terminal' || isBusy || isWorkingAtDraw) {
189      await compactQueued($)
190      return
191    }
192    try {
193      await compactNow($)
194    } catch {
195      await compactQueued($)
196    }
197  } finally {
198    isOwnCompact = false
199  }
200}
201
202export const register: Register = on => {
203  on('session.start', async ($, e, next) => {
204    const started = await next(e)
205    const savedTtl = await $.store.get('ttl')
206    if (savedTtl === '5m' || savedTtl === '1h') await update($, ttl, () => savedTtl)
207    const savedCollapsed = await $.store.get('isCollapsed')
208    if (typeof savedCollapsed === 'boolean') await update($, isCollapsed, () => savedCollapsed)
209
210    const usage = await $.session.usage()
211    await setContext($, usage.context.percent, usage.context.tokens)
212    await readThreshold($)
213    await startTicking($)
214
215    return started
216  })
217
218  // Bei einer fortgesetzten Session: wann die letzte Antwort kam und mit welcher Laufzeit.
219  on('classic.SessionStart', async ($, e, next) => {
220    const result = await next(e)
221    if (e.seconds_since_last_response !== undefined && (await read($, last)) === null) {
222      const at = (await $.clock.now()) - e.seconds_since_last_response * 1000
223      await update($, last, () => ({ at, hitRate: null }))
224    }
225    if (e.source === 'resume' || e.source === 'fork') await readTtlFromTranscript($, e.transcript_path)
226    await startTicking($)
227
228    return result
229  }).catch(($, e, next) => next(e))
230
231  on('turn.start', ($, e, next) => {
232    isBusy = true
233    return next(e)
234  })
235
236  // Jede Antwort des Hauptgesprächs frischt den Cache auf und bringt die Hit-Rate mit.
237  on('turn.step', async function* ($, e, next) {
238    const result = yield* next(e)
239    if (e.agentId === undefined && result.usage !== null) {
240      const response: LastResponse = { at: await $.clock.now(), hitRate: hitRate(result.usage) }
241      await update($, last, () => response)
242      await startTicking($)
243    }
244    return result
245  })
246
247  on('turn.complete', async ($, e, next) => {
248    const result = await next(e)
249    if (e.agentId === undefined) {
250      isBusy = false
251      if (!(await read($, context)).isThresholdRead) await readThreshold($)
252    }
253    return result
254  })
255
256  // Am Ende jeder Runde steht die letzte Antwort im Protokoll: daraus die echte Laufzeit.
257  on('classic.Stop', async ($, e, next) => {
258    const result = await next(e)
259    if (e.agent_id === undefined) await readTtlFromTranscript($, e.transcript_path)
260    return result
261  }).catch(($, e, next) => next(e))
262
263  on('classic.PostModelSwitch', async ($, e, next) => {
264    const result = await next(e)
265    await setTtl($, e.cache_ttl)
266    await update($, context, fill => ({ ...fill, isThresholdRead: false }))
267    await readThreshold($)
268    return result
269  }).catch(($, e, next) => next(e))
270
271  on('session.measure', async ($, e, next) => {
272    if (e.changed.includes('context')) await setContext($, e.context.percent, e.context.tokens)
273    return next(e)
274  })
275
276  on('classic.PostCompact', async ($, e, next) => {
277    const result = await next(e)
278    if (isOwnCompact) return result
279    compactSummary = e.compact_summary
280    if (pendingCompact !== undefined) logCompact($)
281    return result
282  }).catch(($, e, next) => next(e))
283
284  // /compact, selbst getippt (der Knopf schreibt seine Zusammenfassung selbst, siehe compact).
285  on('session.compact', async ($, e, next) => {
286    const before = contextTokens
287    const result = await next(e)
288    if (isOwnCompact || e.agentId !== undefined || result.skip !== undefined) return result
289
290    await resetAfterCompact($)
291    if (e.trigger !== 'auto' && e.trigger !== 'precompute') {
292      pendingCompact = { before: result.tokensBefore ?? before, after: result.tokensAfter, fallback: summaryOf(result.messages), isLogged: false }
293      // Die Zusammenfassung aus PostCompact abwarten, wenn sie nicht schon da ist.
294      if (compactSummary !== undefined) logCompact($)
295      else $.clock.after(1500, () => logCompact($))
296    }
297    return result
298  }).catch(($, e, next) => next(e))
299
300  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
301    if (e.props.hasSurvey) return next(e)
302
303    const { Box, Text, Button } = $.ui.resolve(e)
304    const response = await read($, last)
305    const cacheTtl = await read($, ttl)
306    const ttlKnown = await read($, isTtlRead)
307    const fill = await read($, context)
308    const t = await read($, now)
309    const collapsed = await read($, isCollapsed)
310    const isWorkingAtDraw = e.props.isWorking
311
312    const view = response === null ? undefined : cacheView(response.at, cacheTtl, Math.max(t, response.at))
313    const sky = fill.percent === null ? '☀️' : weather(fill.percent, fill.threshold)
314    const isStorm = sky === '⛈️'
315    const thresholdText = `${fill.isThresholdRead ? '' : '~'}${fill.threshold} %`
316    const percentText = fill.percent === null ? '–' : `${fill.percent} %`
317    const minutesText =
318      view === undefined ? '–' : view.minutes === 0 ? 'abgelaufen' : `${view.minutes} Min`
319    const minutesColor = view === undefined ? undefined : view.color
320
321    const toggle = (
322      <Button
323        key="toggle"
324        plain
325        dimColor
326        label={collapsed ? '▸' : '◂'}
327        onPress={async () => {
328          const value = await update($, isCollapsed, c => !c)
329          await $.store.set('isCollapsed', value)
330        }}
331      />
332    )
333
334    if (collapsed || e.props.bodyColumns < 60) {
335      return (
336        <Box flexDirection="row">
337          <Text>{sky} </Text>
338          <Text color={minutesColor} dimColor={view === undefined}>
339            {minutesText}
340          </Text>
341          <Text dimColor> · </Text>
342          <Text>{percentText} </Text>
343          {toggle}
344        </Box>
345      )
346    }
347
348    const bar =
349      view === undefined ? (
350        <Text dimColor>{'░'.repeat(BAR_CELLS)}</Text>
351      ) : (
352        <Text>
353          <Text color={view.color}>{'█'.repeat(view.filled)}</Text>
354          <Text dimColor>{'░'.repeat(BAR_CELLS - view.filled)}</Text>
355        </Text>
356      )
357    const hit = response?.hitRate ?? null
358
359    return (
360      <Box flexDirection="row">
361        <Text dimColor>Cache{ttlKnown ? ` ${cacheTtl === '1h' ? '1 h' : '5 Min'}` : ''}: </Text>
362        <Text color={minutesColor} dimColor={view === undefined}>
363          {minutesText}{' '}
364        </Text>
365        {bar}
366        <Text dimColor> · Hit </Text>
367        <Text>{hit === null ? '–' : `${hit} %`}</Text>
368        <Text dimColor> · </Text>
369        <Text>{sky} </Text>
370        <Text dimColor>Kontext </Text>
371        <Text color={isStorm ? 'error' : undefined}>{percentText}</Text>
372        <Text dimColor> / Compact bei {thresholdText} </Text>
373        <Button
374          key="compact"
375          label="Compact"
376          variant={isStorm ? 'primary' : undefined}
377          dimColor={isStorm ? undefined : true}
378          onPress={press => compact($, press, isWorkingAtDraw)}
379        />
380        <Text> </Text>
381        {toggle}
382      </Box>
383    )
384  })
385}
386
hooks/logic.ts 166 lines
1import type { CacheTtl } from '../types'
2
3export const BAR_CELLS = 20
4export const FALLBACK_THRESHOLD = 83
5/** Die Zeilen, die das Terminal von einer Log-Zeile zeichnet, sind 2000 Zeichen; etwas Luft bleibt. */
6export const LOG_CHUNK = 1800
7
8/** Eine Sekunde Spielraum, damit ein Tick kurz vor der Minutengrenze nicht eine Minute zu viel zeigt. */
9const SLACK_MS = 1000
10
11export const ttlMs = (ttl: CacheTtl): number => (ttl === '1h' ? 3_600_000 : 300_000)
12
13export type CacheView = {
14  /** Ganze Minuten, aufgerundet; 0 heißt abgelaufen. */
15  minutes: number
16  /** Gefüllte Kästchen von BAR_CELLS. */
17  filled: number
18  color: 'success' | 'warning' | 'error'
19}
20
21export const cacheView = (at: number, ttl: CacheTtl, now: number): CacheView => {
22  const total = ttlMs(ttl)
23  const left = Math.max(0, total - (now - at) - SLACK_MS)
24  const fraction = left / total
25  const minutes = Math.ceil(left / 60_000)
26  const filled = Math.min(BAR_CELLS, Math.ceil(fraction * BAR_CELLS))
27  const color = fraction > 0.25 ? 'success' : fraction > 0.1 ? 'warning' : 'error'
28
29  return { minutes, filled, color }
30}
31
32/**
33 * Die Zeitpunkte nach der Antwort, zu denen die Leiste neu gezeichnet wird:
34 * jede volle Minute, dazu der Wechsel auf Gelb (75 %) und auf Rot (90 %).
35 */
36export const redrawOffsets = (ttl: CacheTtl): number[] => {
37  const total = ttlMs(ttl)
38  const offsets = new Set<number>()
39
40  for (let ms = 60_000; ms <= total; ms += 60_000) offsets.add(ms)
41  offsets.add(Math.round(total * 0.75) + SLACK_MS)
42  offsets.add(Math.round(total * 0.9) + SLACK_MS)
43
44  return [...offsets].sort((a, b) => a - b)
45}
46
47export type Weather = '☀️' | '🌧️' | '⛈️'
48
49export const weather = (percent: number, threshold: number): Weather => {
50  const ratio = percent / threshold
51
52  return ratio <= 0.5 ? '☀️' : ratio <= 0.8 ? '🌧️' : '⛈️'
53}
54
55export type Usage = {
56  input_tokens: number
57  cache_read_input_tokens: number
58  cache_creation_input_tokens: number
59}
60
61/** Anteil der Input-Tokens, die aus dem Cache kamen, in ganzen Prozent. */
62export const hitRate = (usage: Usage): number | null => {
63  const read = usage.cache_read_input_tokens ?? 0
64  const total = (usage.input_tokens ?? 0) + read + (usage.cache_creation_input_tokens ?? 0)
65
66  return total > 0 ? Math.round((read / total) * 100) : null
67}
68
69type TranscriptRow = {
70  type?: string
71  isSidechain?: boolean
72  message?: {
73    usage?: {
74      cache_creation?: {
75        ephemeral_1h_input_tokens?: number
76        ephemeral_5m_input_tokens?: number
77      } | null
78    }
79  }
80}
81
82/**
83 * Liest aus dem Session-Protokoll, mit welcher Laufzeit die letzte Antwort
84 * des Hauptgesprächs in den Cache geschrieben hat; undefined, wenn keine
85 * Antwort etwas geschrieben hat.
86 */
87export const ttlFromTranscript = (text: string): CacheTtl | undefined => {
88  const lines = text.split('\n')
89
90  for (let i = lines.length - 1; i >= 0; i -= 1) {
91    const line = lines[i]?.trim()
92    if (!line || !line.includes('"cache_creation"')) continue
93
94    let row: TranscriptRow
95    try {
96      row = JSON.parse(line) as TranscriptRow
97    } catch {
98      continue
99    }
100    if (row.type !== 'assistant' || row.isSidechain === true) continue
101
102    const creation = row.message?.usage?.cache_creation
103    if (!creation) continue
104    if ((creation.ephemeral_5m_input_tokens ?? 0) > 0) return '5m'
105    if ((creation.ephemeral_1h_input_tokens ?? 0) > 0) return '1h'
106  }
107
108  return undefined
109}
110
111/** Tausender mit Punkt, ohne Intl (die Umgebung des Mods hat keins garantiert). */
112export const formatTokens = (n: number): string =>
113  String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, '.')
114
115const hardSplit = (text: string, max: number): string[] => {
116  const parts: string[] = []
117  let rest = text
118
119  while (rest.length > max) {
120    const space = rest.lastIndexOf(' ', max)
121    const cut = space > max / 2 ? space : max
122    parts.push(rest.slice(0, cut).trimEnd())
123    rest = rest.slice(cut).trimStart()
124  }
125  if (rest.length > 0) parts.push(rest)
126
127  return parts
128}
129
130/**
131 * Teilt einen langen Text an Absätzen in Stücke von höchstens `max` Zeichen;
132 * ein zu langer Absatz wird an Zeilen, eine zu lange Zeile an Wörtern geteilt.
133 */
134export const splitParagraphs = (text: string, max: number = LOG_CHUNK): string[] => {
135  // Jedes Stück mit dem Trenner, der im Text davor stand.
136  const pieces: { text: string; sep: string }[] = []
137
138  for (const paragraph of text.trim().split(/\n\s*\n/)) {
139    if (paragraph.length <= max) {
140      pieces.push({ text: paragraph, sep: '\n\n' })
141      continue
142    }
143    paragraph.split('\n').forEach((line, i) => {
144      const sep = i === 0 ? '\n\n' : '\n'
145      hardSplit(line, max).forEach((part, j) => pieces.push({ text: part, sep: j === 0 ? sep : ' ' }))
146    })
147  }
148
149  const chunks: string[] = []
150  let current = ''
151
152  for (const piece of pieces) {
153    if (current === '') {
154      current = piece.text
155    } else if (current.length + piece.sep.length + piece.text.length > max) {
156      chunks.push(current)
157      current = piece.text
158    } else {
159      current = current + piece.sep + piece.text
160    }
161  }
162  if (current.trim() !== '') chunks.push(current)
163
164  return chunks
165}
166
types/index.d.ts 25 lines
1export type CacheTtl = '5m' | '1h'
2
3/** Die letzte Antwort des Hauptgesprächs: wann sie kam und wie viel aus dem Cache. */
4export type LastResponse = { at: number; hitRate: number | null }
5
6/** Kontextfüllung und Auto-Compact-Schwelle, beides in Prozent des Kontextfensters. */
7export type ContextFill = {
8  percent: number | null
9  threshold: number
10  isThresholdRead: boolean
11}
12
13declare module 'claude-code' {
14  interface PluginState {
15    'cache-leiste': {
16      last: LastResponse | null
17      ttl: CacheTtl
18      isTtlRead: boolean
19      context: ContextFill
20      now: number
21      isCollapsed: boolean
22    }
23  }
24}
25