Pasek nad promptem: kontekst, odliczanie cache, koszt sesji, przyciski Handoff i Wyczyść i kontynuuj

Tu leży wszystko, co rozdaję na kanale. Za darmo, bez zapisywania się na cokolwiek.
Jak pobrać: kliknij zielony przycisk Code u góry, potem Download ZIP. Rozpakuj na pulpit. Nie musisz wiedzieć, co to GitHub — wystarczy ten jeden przycisk.
Skill to instrukcja, którą AI wykona. W czymkolwiek ściągniętym z internetu może być napisane wszystko. Sprawdzaj każdą paczkę, zanim ją wrzucisz. Moją też.
Paczka leży jeszcze w zwykłym folderze na pulpicie, więc otwórz nową rozmowę z AI w tym folderze i wklej:
Zanim tego użyję: przeczytaj te pliki jak zwykły tekst i powiedz mi po ludzku,
co ta instrukcja każe robić. Czy każe coś kasować, wysyłać gdzieś dane,
uruchamiać komendy albo grzebać poza swoim folderem?
Zasada kciuka: jeśli w instrukcji jest „wyślij", „skasuj" albo „uruchom" coś, czego nie rozumiesz — nie instalujesz. Nie musisz rozumieć wszystkiego. Wystarczy, że wiesz, kiedy powiedzieć „nie".
To nie jest kontrola pancerna, ale wyłapuje zdecydowaną większość śmieci. Różnica jak między wzięciem cukierka od obcego a przeczytaniem składu.
| Folder | Co to robi | Odcinek |
|---|---|---|
ghost/ | AI pisze maile i wiadomości twoim stylem, a nie jak robot | „Nauczyłem AI pisać jak ja" |
handoff/ | Kończysz rozmowę tak, żeby nowa wiedziała, na czym stanęliście — mniej zużytego limitu | „Znowu masz limit" |
krotko/ | Jedno słowo i z długiej odpowiedzi AI zostaje kilka zdań po ludzku | „AI gada za dużo" |
mody-limit/ | Trzech pomocników w Claude Code: pasek zużycia w kolorach, przycinanie gadatliwych wyników, stop dla AI kręcącego się w kółko | (odcinek w przygotowaniu) |
Każdy folder ma własny README.md z instrukcją krok po kroku.
Napisz w komentarzu pod odcinkiem — czytam wszystkie i z powtarzających się pytań robię następne filmy.
hooks/register.tsx 244 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, FsEntry, Register, RenderSurface } from 'claude-code'
3
4import type { Handoff } from '../types'
5
6// Cache promptu w tej sesji żyje 60 min od ostatniego zapytania.
7// Po wygaśnięciu następna wiadomość płaci za cały kontekst od nowa.
8const CACHE_TTL_MS = 60 * 60 * 1000
9const WARN_BEFORE_MS = 5 * 60 * 1000
10const MINUTE_MS = 60 * 1000
11
12const lastActivity = atom({ plugin: 'straznik-cache', key: 'lastActivity' } as const, null)
13const tick = atom({ plugin: 'straznik-cache', key: 'tick' } as const, 0)
14const isWarned = atom({ plugin: 'straznik-cache', key: 'isWarned' } as const, false)
15const isHidden = atom({ plugin: 'straznik-cache', key: 'isHidden' } as const, false)
16const handoff = atom({ plugin: 'straznik-cache', key: 'handoff' } as const, null)
17
18// Progi kolorów: zielony → pomarańczowy → czerwony.
19const CONTEXT_ORANGE_PERCENT = 50
20const CONTEXT_RED_PERCENT = 75
21const CACHE_ORANGE_MS = 15 * 60 * 1000
22const LIMIT_ORANGE_PERCENT = 60
23const LIMIT_RED_PERCENT = 85
24
25// Klucze motywu: same dopasowują się do jasnego i ciemnego trybu.
26type Level = 'success' | 'warning' | 'error'
27const byPercent = (percent: number, orange: number, red: number): Level =>
28 percent >= red ? 'error' : percent >= orange ? 'warning' : 'success'
29const byTimeLeft = (left: number): Level =>
30 left <= WARN_BEFORE_MS ? 'error' : left <= CACHE_ORANGE_MS ? 'warning' : 'success'
31
32const thousands = (tokens: number) => `${Math.round(tokens / 1000)}k`
33
34export const register: Register = on => {
35 on('session.start', async ($, e, next) => {
36 await $.command.register({
37 name: 'straznik',
38 description: 'Pokaż albo schowaj pasek Strażnika cache',
39 })
40
41 $.clock.every(MINUTE_MS, async () => {
42 await update($, tick, n => n + 1)
43
44 const last = await read($, lastActivity)
45 if (last === null || (await read($, isWarned))) {
46 return
47 }
48
49 const left = last + CACHE_TTL_MS - (await $.clock.now())
50 if (left > 0 && left <= WARN_BEFORE_MS) {
51 await update($, isWarned, () => true)
52 const { context } = await $.session.usage()
53 const size = context.tokens ? ` (${thousands(context.tokens)} tokenów)` : ''
54 const text = `Cache tej sesji${size} wygasa za ${Math.ceil(left / MINUTE_MS)} min. Napisz coś albo zrób handoff.`
55 $.ui.toast(text, { timeoutMs: 15000 })
56 // Powiadomienie systemowe bywa niedostępne (np. bez terminala) — toast wystarczy.
57 try {
58 await $.ui.notify(text, { title: 'Strażnik cache' })
59 } catch {}
60 }
61 })
62
63 return next(e)
64 })
65
66 on('command.run', { command: 'straznik' }, async $ => {
67 const hidden = await update($, isHidden, was => !was)
68
69 return { text: hidden ? 'Pasek Strażnika schowany.' : 'Pasek Strażnika widoczny.' }
70 })
71
72 on('session.end', async ($, e, next) => {
73 if (e.reason === 'clear') {
74 isCleared = true
75 }
76
77 return next(e)
78 })
79
80 on('turn.complete', async ($, e, next) => {
81 if (e.agentId !== undefined) {
82 return next(e)
83 }
84
85 const now = await $.clock.now()
86 await update($, lastActivity, () => now)
87 await update($, isWarned, () => false)
88
89 const pending = await read($, handoff)
90 if (pending !== null && pending.file === null) {
91 const file = await newestHandoff($, pending.requestedAt)
92 if (file !== null) {
93 await update($, handoff, () => ({ ...pending, file }))
94 }
95 }
96
97 return next(e)
98 })
99
100 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
101 if (e.props.hasSurvey || (await read($, isHidden))) {
102 return next(e)
103 }
104
105 await read($, tick)
106 const last = await read($, lastActivity)
107 if (last === null) {
108 return next(e)
109 }
110
111 const { Box, Button, Text } = $.ui.resolve(e)
112 const { context, cost, rateLimits } = await $.session.usage()
113 const left = last + CACHE_TTL_MS - (await $.clock.now())
114 const pending = await read($, handoff)
115
116 // level === undefined: kawałek bez koloru (koszt sesji), przygaszony.
117 const parts: { text: string; level?: Level }[] = []
118 if (context.percent !== undefined && context.tokens !== undefined) {
119 parts.push({
120 text: `kontekst ${context.percent}% (${thousands(context.tokens)})`,
121 level: byPercent(context.percent, CONTEXT_ORANGE_PERCENT, CONTEXT_RED_PERCENT),
122 })
123 }
124 parts.push({
125 text:
126 left > 0
127 ? `cache ${Math.ceil(left / MINUTE_MS)} min`
128 : 'cache wygasł — następna wiadomość czyta wszystko od nowa',
129 level: byTimeLeft(left),
130 })
131 if (cost !== undefined) {
132 parts.push({ text: `sesja $${cost.usd.toFixed(2)}` })
133 }
134 const fiveHour = rateLimits.find(limit => limit.kind === 'five_hour')
135 if (fiveHour !== undefined) {
136 parts.push({
137 text: `limit 5h ${fiveHour.percentUsed}%`,
138 level: byPercent(fiveHour.percentUsed, LIMIT_ORANGE_PERCENT, LIMIT_RED_PERCENT),
139 })
140 }
141
142 return (
143 <Box>
144 <Text>
145 {parts.flatMap((part, i) => [
146 ...(i > 0 ? [<Text dimColor>{' · '}</Text>] : []),
147 part.level === undefined
148 ? <Text dimColor>{part.text}</Text>
149 : <Text color={part.level}>{part.text}</Text>,
150 ])}{' '}
151 </Text>
152 {!e.props.isWorking && pending?.file == null && (
153 <Button
154 key="handoff"
155 label="Handoff"
156 onPress={() => startHandoff($)}
157 />
158 )}
159 {!e.props.isWorking && pending?.file != null && (
160 <Button
161 key="clear"
162 label="Wyczyść i kontynuuj"
163 onPress={() => clearAndContinue($, e.surface, pending.file ?? '')}
164 />
165 )}
166 </Box>
167 )
168 })
169}
170
171// Ustawiane przez session.end, gdy /clear naprawdę wyczyścił rozmowę.
172let isCleared = false
173
174// prompt.submit nie przyjmuje tekstu zaczynającego się od „/”, więc skill
175// odpalamy jako komendę; gdyby nie przeszła, wysyłamy zwykłe zdanie.
176async function startHandoff($: EngineInterface) {
177 const now = await $.clock.now()
178 const pending = await read($, handoff)
179 if (pending !== null && pending.file === null && now - pending.requestedAt < 2 * MINUTE_MS) {
180 $.ui.toast('Handoff już się robi — poczekaj na kartkę.')
181 return
182 }
183
184 await update($, handoff, (): Handoff => ({ requestedAt: now, file: null }))
185 $.ui.toast('Robię handoff…')
186
187 const ran = await $.command.run({ command: 'handoff' }).catch(err => ({ error: String(err) }))
188 if (!('error' in ran)) {
189 return
190 }
191
192 const sent = await $.prompt
193 .submit({ text: 'Zrób handoff tej sesji (skill handoff).', asUser: true })
194 .catch(err => ({ error: String(err) }))
195 if ('error' in sent || sent.drop !== undefined) {
196 await update($, handoff, () => null)
197 $.ui.toast('Nie udało się uruchomić handoffu z paska — wpisz /handoff ręcznie.', { timeoutMs: 15000 })
198 }
199}
200
201async function clearAndContinue($: EngineInterface, surface: RenderSurface, file: string) {
202 const line = `Przeczytaj \`${file}\` i wracamy do roboty.`
203
204 // Schowek najpierw: to jest wyjście awaryjne, gdyby /clear z moda nie przeszedł.
205 await $.ui.copy({ text: line, surface }).catch(() => undefined)
206
207 isCleared = false
208 await $.command.run({ command: 'clear' }).catch(() => undefined)
209
210 if (!isCleared) {
211 $.ui.toast('Mod nie zdołał wyczyścić rozmowy. Linijka jest w schowku: wpisz /clear, potem wklej (Ctrl+V).', { timeoutMs: 20000 })
212 return
213 }
214
215 await update($, handoff, () => null)
216 await update($, lastActivity, () => null)
217 const sent = await $.prompt.submit({ text: line, asUser: true }).catch(err => ({ error: String(err) }))
218 if ('error' in sent || sent.drop !== undefined) {
219 await $.prompt.fill({ text: line }).catch(() => undefined)
220 }
221}
222
223// Najnowsza kartka zapisana po kliknięciu Handoff. Skill handoff zapisuje ją
224// w folderze projektu (`handoff\`) albo w `~/.claude/handoff` — szukamy w obu.
225async function newestHandoff($: EngineInterface, since: number): Promise<string | null> {
226 const home = (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME'))
227 const places = [
228 { dir: 'handoff', shown: 'handoff/' },
229 ...(home === undefined ? [] : [{ dir: `${home}/.claude/handoff`, shown: '~/.claude/handoff/' }]),
230 ]
231
232 const found: { path: string; mtimeMs: number }[] = []
233 for (const place of places) {
234 const entries: FsEntry[] = await $.fs.list(place.dir).catch(() => [])
235 for (const entry of entries) {
236 if (entry.kind === 'file' && entry.name.endsWith('.md') && entry.mtimeMs >= since) {
237 found.push({ path: `${place.shown}${entry.name}`, mtimeMs: entry.mtimeMs })
238 }
239 }
240 }
241
242 return found.sort((a, b) => b.mtimeMs - a.mtimeMs)[0]?.path ?? null
243}
244types/index.d.ts 22 lines1export type Handoff = {
2 /** Kiedy kliknięto Handoff (ms od epoki). */
3 requestedAt: number
4 /** Ścieżka kartki (`handoff/…md` w projekcie albo `~/.claude/handoff/…md`), gdy już powstała. */
5 file: string | null
6}
7
8declare module 'claude-code' {
9 interface PluginState {
10 'straznik-cache': {
11 /** Koniec ostatniej tury głównej rozmowy (ms od epoki); null przed pierwszą. */
12 lastActivity: number | null
13 /** Licznik minut, żeby pasek przerysował odliczanie. */
14 tick: number
15 /** Czy ostrzeżenie „cache wygasa” już poszło dla tej przerwy. */
16 isWarned: boolean
17 isHidden: boolean
18 handoff: Handoff | null
19 }
20 }
21}
22