SLOPSHOPPER

kanon-flota

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…

newpanecommandtoastprocesstimer
v0.1.0MITupdated 2026-10-11stelmach-systems/claude-mods/kanon-flota
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · kanon-flota
│ ┃ Flota ✕ › fix the failing auth test and add an audit log call │ ┃ Odczyt… │ ┃ o: odśwież 0 sesji (0 z modem) · odczyt — co ⏺ 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 │ │ › /flota │ ⎿ kanon-flota: Flota: 0 sesji (0 z modem) · odczyt — co 10 s · STA │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Flota
Odczyt… o: odśwież 0 sesji (0 z modem) · odczyt — co 10 s · STARE DA
README

kanon-flota — pane „flota sesji”

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
  • Lista sesji: 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.
  • Stan z czasem: ● 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.
  • ctx %, koszt, ↓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.
  • Wiadomości między sesjami: ↓ = 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.
  • Ping: przycisk przy wierszu (nie przy własnej sesji) → $.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://).
  • Sprzątanie: wpis flota:* sesji nieobecnej w claude agents i bez zapisu od doby → $.store.delete (tylko po udanym odczycie).

Odstępstwa od briefu (sprawdzone na d.ts 2.1.295 i w kicie testowym)

  1. lastInFrom → lastInKind (peer, peer:<plugin>, peer-send-message): SessionReceiveInput nie niesie adresu nadawcy; adres widać u nadawcy z modem (lastOutTo).
  2. 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.
  3. Własny $.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).
  4. Jeden timer 10 s od 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).
  5. msgOut liczy wysyłki doręczone (isDelivered), nie próby.

Bramki

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).

Testy

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 *).

Recenzja (sceptyk [OP5.5HI], 1 runda, 2026-10-11)

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;
  • czy ping z pane przychodzi u odbiorcy jako 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);
  • czy peer bez teammate przychodzi też od własnych agentów sesji (zawyżyłby ↓);
  • render przy ~110 kolumnach terminala i w pane desktopu; $.state po /clear.

Odczyty biegną także, gdy pane jest ukryty za kartą (świadomie: ciągłość czasów stanu).

Uruchomienie i rollback

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).

Source 2 files
hooks/register.tsx 268 lines
1// 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}
268
types/index.d.ts 65 lines
1// 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