Pane „flota sesji”: wszystkie żywe sesje Claude Code na tym Macu z `claude agents --json` (stan z czasem, projekt, nazwa), ctx %, koszt i liczniki wiadomości…

Mod Claude Code (API „function hooks”, early access; pisany pod 2.1.295, testy zielone na desktop 2.1.295 i CLI 2.1.289). Z briefu modu (2026-10-11, opcja B). /flota otwiera pane z WSZYSTKIMI żywymi sesjami Claude Code na tym Macu — także desktopowymi i tymi bez modu.
ping ● pracuje 2 min · api-serwis · ctx 41% · $3.12 · ↓0 ↑1 · Migracja bazy
ping ◐ czeka na Ciebie (input needed) od ≥ 4 min · raporty · bez modu · Raport kwartalny etap 2
ping ○ bezczynna od 1 h 12 min · strona-www · bez modu · Poprawki formularza zgłoszeń
ta ● pracuje ≥ 0 s · mody · ctx 22% · $4.80 · ↓1 ↑0 · (ta sesja) · Nowy mod z planu
odśwież 4 sesji (2 z modem) · odczyt 12:03:10 co 10 s
claude agents --json (docs agent-view: wspierana droga odczytu stanu sesji z zewnątrz; ~0,35 s), co 10 s, TYLKO gdy pane jest otwarty ($.ui.panes()), z CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 (bez tej zmiennej każde wywołanie łączy się z api.anthropic.com — zmierzone proxy: 2× CONNECT bez, 0× z). Odczyty się nie nakładają: /flota, o i timer w trakcie odczytu dołączają do bieżącego. claude spoza PATH → $HOME/.local/bin/claude. Błąd startu, kod ≠ 0 albo zły JSON → poprzednia lista + „STARE DANE · błąd: …” w stopce; pane żyje. o (albo przycisk) = odśwież teraz.● pracuje, ◐ czeka na Ciebie (z waitingFor), ○ bezczynna. --json nie podaje czasu zmiany statusu, więc pane liczy go sam od chwili, gdy zmianę zobaczył; ≥ = dolna granica (status był już taki przy pierwszym odczycie albo poprzedni odczyt był nieudany lub starszy niż 15 s — np. pane był zamknięty — więc zmiana mogła zajść w przerwie). Nazwa sesji stoi na końcu wiersza: przy wąskim pane obcina się ona, a nie ctx/koszt/liczniki.↓in ↑out: tylko dla sesji z tym modem — każda zapisuje swój wpis flota:<sid> w $.store (jeden plik JSON modu, wspólny dla sesji na maszynie; pisze tylko właściciel klucza) przy starcie, początku i końcu tury, wiadomości i co 30 s. Sesja bez modu: „bez modu”. Po restarcie procesu liczniki wracają z własnego wpisu; /clear (nowa rozmowa, nowe id) zeruje je.↓ = dostarczenia do głównej rozmowy z session.receive o origin.kind peer (bez teammate) lub peer-send-message; ↑ = doręczone wysyłki głównej rozmowy (SendMessage modelu poza własnymi agentami + ping z pane). Hooki tylko liczą po next — nie zatrzymują ani nie zmieniają wiadomości.$.session.send({ to: { sessionId }, text: "Status? Jedna linia." }); toast „doręczony” albo powód odmowy. Ping to TURA w sesji odbiorcy (koszt jej kontekstu). Wejście do sesji = klik w pasku bocznym desktopu (Link w desktopie nie otwiera claude://).flota:* sesji nieobecnej w claude agents i bez zapisu od doby → $.store.delete (tylko po udanym odczycie).lastInFrom → lastInKind (peer, peer:<plugin>, peer-send-message): SessionReceiveInput nie niesie adresu nadawcy; adres widać u nadawcy z modem (lastOutTo).msgIn liczy też peer, nie tylko peer-send-message: wiadomość z $.session.send innej sesji przychodzi jako peer z plugin (d.ts SessionReceiveOrigin) — bez tego ping z pane nie podbiłby ↓ u odbiorcy.$.session.send modu nie przechodzi przez jego własny hook session.send (kit 2.1.295) — ↑ za ping liczy ping(), a hook pomija wysyłki z origin.name === 'kanon-flota' (brak podwójnego liczenia, gdyby żywy silnik jednak wołał hook).session.start zamiast timera start/stop przy pane: odczyt i tak tylko przy otwartym pane, a po hot reloadzie session.start zakłada timer od nowa (timer z /flota zginąłby razem z modułem).msgOut liczy wysyłki doręczone (isDelivered), nie próby.Zero $.model, zero $.http, zero $.fs, zero tool.call/tool.check; zapis tylko do $.store i $.state. W store: id sesji, cwd, id modelu, ctx %, koszt, liczniki, rodzaj ostatniej przychodzącej, adres ostatniej wysyłki, czasy — bez treści wiadomości i rozmów (test: po pingu i dwóch wiadomościach store nie zawiera ich tekstu). Nazwy sesji i cwd innych sesji pokazuje tylko pane ($.state); /flota wpisuje do rozmowy tylko stopkę (liczba sesji, czas), pełną listę — gdy pane nie może się otworzyć.
Linie walidatora (claude plugin validate, 2.1.289 i 2.1.295):
hooks: session.start, turn.start, turn.complete, session.end, session.receive, session.send, command.run{command=flota}, ui.render{component=Pane, requestId=kanon-flota}
calls: $.agent.list, $.clock.every, $.clock.now, $.command.register, $.env.get (via agenci), $.process.run (via agenci), $.session.cwd (via zapisz), $.session.id, $.session.model (via zapisz), $.session.send (via ping), $.session.usage (via zapisz), $.state.get, $.state.set, $.store.delete (via czytaj), $.store.get, $.store.keys (via czytaj), $.store.set (via zapisz), $.ui.open, $.ui.panes, $.ui.resolve, $.ui.toast (via ping)
env reads: HOME
$.process.run uruchamia wyłącznie claude agents --json (argv bez shella, timeout 5 s, env jak wyżej).
claude plugin test . — 11 testów, zielone na CLI 2.1.289 i desktop 2.1.295 (pane na terminalu i desktopie, timer i czasy stanu, ping doręczony/niedoręczony bez tekstu w store, liczenie session.receive, fallback PATH + błędy + zły JSON + env bez ruchu sieciowego, sprzątanie store i powrót liczników, przerwa w odczytach → ≥, SendMessage modelu liczony poza własnymi agentami + /clear, brak nakładania odczytów). Każdy z 5 testów poprawek po recenzji pada po cofnięciu swojej poprawki (mutacje sprawdzone na kopii). Type-check: tsc 5.9.3 strict z tsconfig z nagłówka d.ts (silnik kładzie typy w .claude-plugin/types/ przy pierwszym załadowaniu, z własnym .gitignore *).
Naprawione: 🔴 claude agents łączył się z api.anthropic.com co 10 s (wbrew „zero sieci”) → env w process.run; 🟡 czas stanu po przerwie w odczytach podawany jako pewny → próg ciągłości 15 s; 🟡 nakładające się odczyty przy wolnym CLI → wspólna obietnica, timeout 10 → 5 s; 🟡 nazwa przed ctx/kosztem obcinała liczby → nazwa na końcu; 🟢 długa nazwa pluginu w lastInKind → skroc; 🟢 liczniki przeżywały /clear → zerowanie w session.end.
Nierozstrzygnięte w kicie (kontrola na żywo w dwóch sesjach):
$.store między procesami: czy sesja widzi wpisy innych i czy zapis całego pliku jednej sesji nie gubi wpisów pozostałych;peer z plugin (lastInKind = peer:kanon-flota) i czy żywy silnik woła własny hook session.send dla $.session.send modu (filtr origin.name — w kicie origin stempluje silnik);peer bez teammate przychodzi też od własnych agentów sesji (zawyżyłby ↓);$.state po /clear.Odczyty biegną także, gdy pane jest ukryty za kartą (świadomie: ciągłość czasów stanu).
Na stałe: ścieżka modu w env.CLAUDE_CODE_PLUGIN_DIRS w ~/.claude/settings.json (z backupem), działa od NOWEJ sesji. Jedna sesja z terminala: claude --plugin-dir ~/Developer/claude-mods/kanon-flota. Rollback: usunięcie ścieżki z CLAUDE_CODE_PLUGIN_DIRS (od nowej sesji) + rm -f ~/.claude/plugins/store/kanon-flota* (dane modu: liczniki i czasy).
hooks/register.tsx 268 lines1// kanon-flota — pane „flota sesji” (mod Claude Code, API function hooks 2.1.295; early access). v0.1.
2// Wszystkie żywe sesje Claude Code na tym Macu z `claude agents --json` (docs agent-view: wspierana droga odczytu stanu sesji z zewnątrz),
3// scalone po `sessionId` z wpisami `flota:<sid>`, które ten mod zapisuje w `$.store` (wspólny dla sesji na maszynie) w każdej sesji, gdzie
4// jest załadowany: ctx %, koszt, liczniki wiadomości między sesjami. Sesja bez modu też jest na liście — bez ctx i kosztu.
5// Bramki: zero wywołań modelu, zero sieci, nie zatwierdza narzędzi, zapis tylko do `$.store`/`$.state`; w store żadnej treści wiadomości
6// ani rozmów — liczby, rodzaje, adresy, czasy. Hooki `session.receive`/`session.send` tylko liczą po `next` (nigdy `consumed`), `.catch` → `next(e)`.
7// Czego NIE wiadomo: od kiedy sesja ma dany status (`--json` nie podaje czasu zmiany) — liczymy od pierwszego odczytu, z „≥”, gdy to dolna granica;
8// kto przysłał wiadomość (`session.receive` nie niesie adresu nadawcy) — zapisujemy rodzaj, adres widać u nadawcy z modem (`lastOutTo`).
9import { atom, read, update } from 'claude-code'
10import type { EngineInterface, Register } from 'claude-code'
11
12import type { Flota, Liczniki, Wiersz, Wpis } from '../types'
13
14const PANE = 'kanon-flota'
15const PING = 'Status? Jedna linia.'
16const KLUCZ = 'flota:'
17const ODSWIEZ = 10_000
18const DOBA = 86_400_000
19
20// Skaner walidatora wymaga literałów plugin/key w każdym atomie — stąd powtórzona nazwa.
21const flota = atom({ plugin: 'kanon-flota', key: 'flota' } as const, { wiersze: [], odczyt: null, blad: null, stare: false } as Flota)
22const ZERO: Liczniki = { msgIn: 0, msgOut: 0, lastInKind: null, lastInAt: null, lastOutTo: null, lastOutAt: null }
23const liczniki = atom({ plugin: 'kanon-flota', key: 'liczniki' } as const, ZERO)
24
25type Agent = { sessionId: string; cwd?: string; name?: string; status?: string; waitingFor?: string }
26
27const IKONA: Record<string, string> = { busy: '●', waiting: '◐', idle: '○' }
28const skroc = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 1)}…` : s)
29const projekt = (cwd: string) => cwd.split('/').filter(Boolean).pop() ?? cwd
30const czas = (ms: number) => {
31 const m = Math.floor(ms / 60_000)
32 return ms < 60_000 ? `${Math.max(0, Math.round(ms / 1000))} s` : m < 60 ? `${m} min` : `${Math.floor(m / 60)} h ${m % 60} min`
33}
34const blad = (err: unknown) => skroc(err instanceof Error ? err.message : String(err), 160)
35
36/**
37 * Wpis tej sesji do `$.store` — liczniki z `$.state` (prawda sesji), ctx i koszt z `$.session.usage()` (wywołanie bez breakdown jest darmowe).
38 * Po `update` podaj zapisane liczniki: odczyt `$.state` w tym samym dispatchu może widzieć stan sprzed zapisu.
39 */
40async function zapisz($: EngineInterface, swieze?: Liczniki): Promise<void> {
41 const [sid, cwd, model, u, l, teraz] = await Promise.all([
42 $.session.id(),
43 $.session.cwd(),
44 $.session.model(),
45 $.session.usage(),
46 swieze ?? read($, liczniki),
47 $.clock.now(),
48 ])
49 const wpis: Wpis = { sid, cwd, model, ctx: u.context.percent ?? null, usd: u.cost?.usd ?? null, ...l, updatedAt: teraz }
50 await $.store.set(KLUCZ + sid, wpis)
51}
52const zapiszCicho = ($: EngineInterface) => zapisz($).catch(() => {})
53
54/**
55 * `claude agents --json`; `claude` spoza PATH → `$HOME/.local/bin/claude`. Rzuca z opisem błędu (start, kod wyjścia, JSON).
56 * Env: CLI 2.1.289 bez niego łączy się przy KAŻDYM wywołaniu z api.anthropic.com (proxy-log, recenzja 2026-10-11) — mod ma być bez sieci.
57 * Timeout 5 s (odczyt ~0,35 s): z fallbackiem najwyżej 10 s, czyli nie dłużej niż okres timera.
58 */
59async function agenci($: EngineInterface): Promise<Agent[]> {
60 const argv = ['agents', '--json']
61 const init = { timeoutMs: 5_000, env: { CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1' } }
62 let r
63 try {
64 r = await $.process.run(['claude', ...argv], init)
65 } catch {
66 r = await $.process.run([`${(await $.env.get('HOME')) ?? ''}/.local/bin/claude`, ...argv], init)
67 }
68 if (r.exitCode !== 0) throw new Error(`claude agents: kod ${r.exitCode}${r.stderr.trim() ? ` — ${r.stderr.trim()}` : ''}`)
69 const d: unknown = JSON.parse(r.stdout)
70 if (!Array.isArray(d)) throw new Error('claude agents: odpowiedź nie jest tablicą')
71 return d.filter((a): a is Agent => typeof a?.sessionId === 'string')
72}
73
74// Jeden odczyt naraz: timer, `/flota` i „odśwież” dołączają do trwającego (starszy wynik nie nadpisze nowszego).
75let trwa: Promise<Flota> | null = null
76const odczyt = ($: EngineInterface): Promise<Flota> => (trwa ??= czytaj($).finally(() => (trwa = null)))
77
78/** Odczyt floty do `$.state`; błąd → poprzednie wiersze + „stare dane”. Zwraca zapisaną wartość (odczyt `$.state` w tym samym dispatchu widzi stan sprzed zapisu). */
79async function czytaj($: EngineInterface): Promise<Flota> {
80 const [teraz, sid] = await Promise.all([$.clock.now(), $.session.id()])
81 let lista: Agent[] | null = null
82 let err: string | null = null
83 try {
84 lista = await agenci($)
85 } catch (e) {
86 err = blad(e)
87 }
88
89 const wpisy = new Map<string, Wpis>()
90 try {
91 for (const k of await $.store.keys()) {
92 if (!k.startsWith(KLUCZ)) continue
93 const w = (await $.store.get(k)) as Wpis | undefined
94 if (!w) continue
95 // Sprzątanie: sesja nieobecna w `claude agents` i bez zapisu od doby. Tylko po udanym odczycie listy.
96 if (lista && !lista.some(a => a.sessionId === w.sid) && teraz - w.updatedAt > DOBA) await $.store.delete(k)
97 else wpisy.set(w.sid, w)
98 }
99 } catch (e) {
100 err ??= `store: ${blad(e)}`
101 }
102
103 return update($, flota, f => {
104 if (!lista) return { ...f, blad: err, stare: true }
105 // Czas stanu = ciągła obserwacja. Po przerwie w odczytach (pane zamknięty, odczyt się nie udał) status mógł się zmienić
106 // i wrócić niewidziany — wtedy liczymy od nowa z „≥” (jedyna pewna dolna granica), także przy tym samym statusie.
107 const ciagly = f.odczyt !== null && !f.stare && teraz - f.odczyt <= 1.5 * ODSWIEZ
108 const przed = new Map(f.wiersze.map(w => [w.sid, w] as const))
109 const wiersze = lista.map((a): Wiersz => {
110 const p = ciagly ? przed.get(a.sessionId) : undefined
111 const status = a.status ?? '?'
112 const ten = p?.status === status
113 return {
114 sid: a.sessionId,
115 projekt: projekt(a.cwd ?? ''),
116 nazwa: a.name ?? '',
117 status,
118 waitingFor: a.waitingFor ?? null,
119 od: ten && p ? p.od : teraz,
120 odPewne: ten && p ? p.odPewne : p !== undefined,
121 wlasna: a.sessionId === sid,
122 wpis: wpisy.get(a.sessionId) ?? null,
123 }
124 })
125 return { wiersze, odczyt: teraz, blad: err, stare: false }
126 })
127}
128
129async function liczWyslane($: EngineInterface, do_: string): Promise<void> {
130 const teraz = await $.clock.now()
131 await zapisz($, await update($, liczniki, l => ({ ...l, msgOut: l.msgOut + 1, lastOutTo: do_, lastOutAt: teraz })))
132}
133
134const stan = (w: Wiersz, teraz: number) => {
135 const t = `${w.odPewne ? '' : '≥ '}${czas(teraz - w.od)}`
136 if (w.status === 'busy') return `pracuje ${t}`
137 if (w.status === 'waiting') return `czeka na Ciebie${w.waitingFor ? ` (${w.waitingFor})` : ''} od ${t}`
138 if (w.status === 'idle') return `bezczynna od ${t}`
139 return `${w.status} od ${t}`
140}
141const linia = (w: Wiersz, teraz: number) => {
142 const m = w.wpis
143 const zModu = m ? `ctx ${m.ctx ?? '?'}% · ${m.usd === null ? '$?' : `$${m.usd.toFixed(2)}`} · ↓${m.msgIn} ↑${m.msgOut}` : 'bez modu'
144 // Nazwa na końcu: `truncate-end` w wąskim docku (od 110 kolumn) obcina ją, a nie liczby.
145 return `${IKONA[w.status] ?? '?'} ${stan(w, teraz)} · ${w.projekt} · ${zModu}${w.wlasna ? ' · (ta sesja)' : ''} · ${skroc(w.nazwa, 40)}`
146}
147const godzina = (ms: number) => new Date(ms).toTimeString().slice(0, 8)
148const stopka = (f: Flota) =>
149 `${f.wiersze.length} sesji (${f.wiersze.filter(w => w.wpis).length} z modem) · odczyt ${f.odczyt === null ? '—' : godzina(f.odczyt)} co ${ODSWIEZ / 1000} s` +
150 (f.stare ? ' · STARE DANE' : '') +
151 (f.blad ? ` · błąd: ${f.blad}` : '')
152
153/** Własny `$.session.send` nie przechodzi przez własny hook `session.send` (test kitu 2.1.295) — ping liczy się tutaj, hook go pomija. */
154async function ping($: EngineInterface, w: Wiersz): Promise<void> {
155 const r = await $.session.send({ to: { sessionId: w.sid }, text: PING })
156 if (r.isDelivered) await liczWyslane($, w.sid).catch(() => {})
157 void $.ui.toast(r.isDelivered ? `ping → ${w.projekt} · ${skroc(w.nazwa, 30)}: doręczony` : `ping → ${w.projekt}: nie doręczony — ${r.reason}`)
158}
159
160export const register: Register = on => {
161 on('session.start', async ($, e, next) => {
162 await $.command.register({ name: 'flota', description: 'Flota sesji Claude Code na tym Macu: stan, projekt, ctx, koszt, wiadomości, ping', immediate: true })
163 try {
164 // Po restarcie procesu `$.state` startuje od zera — liczniki wracają z własnego wpisu w store (po hot reloadzie zostają w `$.state`).
165 const { version } = await $.state.get({ plugin: 'kanon-flota', key: 'liczniki' })
166 const w = version === 0 ? ((await $.store.get(KLUCZ + (await $.session.id()))) as Wpis | undefined) : undefined
167 const l = w
168 ? await update($, liczniki, () => ({ msgIn: w.msgIn, msgOut: w.msgOut, lastInKind: w.lastInKind, lastInAt: w.lastInAt, lastOutTo: w.lastOutTo, lastOutAt: w.lastOutAt }))
169 : undefined
170 await zapisz($, l)
171 } catch {
172 // zbieracz: błąd nie dotyka startu sesji
173 }
174 // Timery żyją do reloadu modułu; `session.start` po reloadzie zakłada je od nowa. Odczyt floty tylko przy otwartym pane.
175 $.clock.every(30_000, () => void zapiszCicho($))
176 $.clock.every(ODSWIEZ, () => {
177 void $.ui
178 .panes()
179 .then(p => (p.some(x => x.id === PANE) ? odczyt($) : undefined))
180 .catch(() => {})
181 })
182 return next(e)
183 })
184
185 on('turn.start', async ($, e, next) => {
186 const r = await next(e)
187 await zapiszCicho($)
188 return r
189 })
190
191 on('turn.complete', async ($, e, next) => {
192 const r = await next(e)
193 if (!e.agentId) await zapiszCicho($)
194 return r
195 })
196
197 // `/clear`: proces idzie dalej pod nowym id, bez `session.start` — liczniki starej rozmowy nie przechodzą na nową.
198 on('session.end', async ($, e, next) => {
199 const r = await next(e)
200 if (e.reason === 'clear') await update($, liczniki, () => ZERO).catch(() => {})
201 return r
202 })
203
204 on('session.receive', async ($, e, next) => {
205 const r = await next(e)
206 try {
207 const o = e.origin
208 // Wiadomość innej sesji do głównej rozmowy: `peer` (model albo plugin innej sesji; bez `teammate` = nie skrzynka zespołu) lub `peer-send-message`.
209 if (!e.agentId && !r.consumed && (o.kind === 'peer-send-message' || (o.kind === 'peer' && !('teammate' in o)))) {
210 const rodzaj = 'plugin' in o && o.plugin ? `${o.kind}:${skroc(o.plugin, 40)}` : o.kind // nazwa pluginu = deklaracja nadawcy
211 const teraz = await $.clock.now()
212 await zapisz($, await update($, liczniki, l => ({ ...l, msgIn: l.msgIn + 1, lastInKind: rodzaj, lastInAt: teraz })))
213 }
214 } catch {
215 // obserwator: błąd liczenia nie dotyka dostarczenia
216 }
217 return r
218 }).catch(($, e, next) => next(e))
219
220 on('session.send', async ($, e, next) => {
221 const r = await next(e)
222 try {
223 // Główna rozmowa (bez `agentId`), doręczone, nie ping tego modu (liczy go `ping`), nie do własnego subagenta/teammate’a.
224 const wlasny = e.origin.kind === 'plugin' && e.origin.name === 'kanon-flota'
225 if (r.isDelivered && !e.agentId && !wlasny && !(await $.agent.list()).some(a => a.id === e.to || a.teammateId === e.to || a.name === e.to)) {
226 await liczWyslane($, e.to)
227 }
228 } catch {
229 // obserwator: błąd liczenia nie dotyka wysyłki
230 }
231 return r
232 }).catch(($, e, next) => next(e))
233
234 on('command.run', { command: 'flota' }, async $ => {
235 const o = await $.ui.open({ id: PANE, title: 'Flota', focus: true, closeOnEscape: true })
236 const f = await odczyt($)
237 if (o.isPlaced) return { text: `Flota: ${stopka(f)}` }
238 const teraz = await $.clock.now()
239 return { text: [`Flota (pane czeka: ${o.reason}): ${stopka(f)}`, ...f.wiersze.map(w => `- ${linia(w, teraz)}`)].join('\n') }
240 })
241
242 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
243 const { Box, Button, Text } = $.ui.resolve(e)
244 const [f, teraz] = await Promise.all([read($, flota), $.clock.now()])
245 const kolor = (s: string) => (s === 'waiting' ? 'warning' : s === 'busy' ? 'success' : undefined)
246
247 return (
248 <Box flexDirection="column">
249 {f.wiersze.length === 0 && <Text dimColor>{f.odczyt === null ? 'Odczyt…' : 'Brak żywych sesji.'}</Text>}
250 {f.wiersze.map(w => (
251 <Box key={w.sid} gap={1}>
252 {w.wlasna ? <Text dimColor>{' ta '}</Text> : <Button key={`ping-${w.sid}`} plain label="ping" onPress={() => ping($, w)} />}
253 <Text wrap="truncate-end" color={kolor(w.status)} dimColor={w.status === 'idle'}>
254 {linia(w, teraz)}
255 </Text>
256 </Box>
257 ))}
258 <Box gap={1}>
259 <Button key="odswiez" plain hotkey="o" label="odśwież" onPress={() => odczyt($)} />
260 <Text wrap="truncate-end" dimColor={!f.stare && !f.blad} color={f.stare || f.blad ? 'warning' : undefined}>
261 {stopka(f)}
262 </Text>
263 </Box>
264 </Box>
265 )
266 })
267}
268types/index.d.ts 65 lines1// Kontrakt modu kanon-flota: `$.state` sesji (pane i liczniki) oraz wpis w `$.store` (wspólny dla sesji na maszynie, klucz `flota:<sid>`).
2
3/** Liczniki wiadomości między sesjami tej sesji — bez treści, tylko liczby, rodzaje, adresy i czasy (`$.clock.now()`). */
4export type Liczniki = {
5 /** Dostarczenia do głównej rozmowy z `session.receive` o `origin.kind` `peer` (bez `teammate`) albo `peer-send-message`. */
6 msgIn: number
7 /** Doręczone (`isDelivered`) wysyłki głównej rozmowy z `session.send` — model (SendMessage) albo plugin (`$.session.send`). */
8 msgOut: number
9 /** Rodzaj ostatniej przychodzącej: `peer`, `peer:<plugin>` albo `peer-send-message`. Adresu nadawcy `session.receive` nie niesie. */
10 lastInKind: string | null
11 lastInAt: number | null
12 /** Adres ostatniej wysyłki, jak go pisze silnik (`e.to`). */
13 lastOutTo: string | null
14 lastOutAt: number | null
15}
16
17/** Wpis sesji z modem w `$.store` pod `flota:<sid>`; pisze go wyłącznie ta sesja. */
18export type Wpis = Liczniki & {
19 sid: string
20 cwd: string
21 model: string
22 /** `$.session.usage().context.percent`; null, gdy silnik go nie ma (przed pierwszą odpowiedzią). */
23 ctx: number | null
24 /** `$.session.usage().cost.usd`; null, gdy host nie prowadzi kosztu. */
25 usd: number | null
26 updatedAt: number
27}
28
29/** Wiersz pane: sesja z `claude agents --json`, uzupełniona wpisem z `$.store`, gdy sesja ma mod. */
30export type Wiersz = {
31 sid: string
32 /** Ostatni segment `cwd`. */
33 projekt: string
34 nazwa: string
35 /** `busy` | `idle` | `waiting` (albo inne słowo przyszłego CLI — pokazywane surowo). */
36 status: string
37 waitingFor: string | null
38 /** Od kiedy widzimy ten status (`$.clock.now()`); `claude agents --json` nie podaje czasu zmiany. */
39 od: number
40 /** false: status był już taki przy pierwszym odczycie albo po przerwie w odczytach — czas to dolna granica („≥”). */
41 odPewne: boolean
42 wlasna: boolean
43 /** null dla sesji bez modu (brak wpisu w store). */
44 wpis: Wpis | null
45}
46
47export type Flota = {
48 wiersze: Wiersz[]
49 /** Czas ostatniego udanego odczytu `claude agents --json`; null przed pierwszym. */
50 odczyt: number | null
51 /** Ostatni błąd odczytu (start procesu, kod wyjścia, JSON); null po udanym. */
52 blad: string | null
53 /** true: ostatni odczyt się nie udał i wiersze pochodzą z poprzedniego. */
54 stare: boolean
55}
56
57declare module 'claude-code' {
58 interface PluginState {
59 'kanon-flota': {
60 flota: Flota
61 liczniki: Liczniki
62 }
63 }
64}
65