@threadcrumbs/common 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/package.json +9 -1
- package/telemetry/README.md +2 -0
- package/telemetry/claude-code.mjs +3 -2
- package/ui/format.mjs +60 -0
- package/ui/html.mjs +12 -1
- package/ui/icons.mjs +105 -98
- package/ui/pictures.css +100 -0
- package/ui/pictures.mjs +158 -0
- package/ui/rows.css +403 -0
- package/ui/rows.mjs +172 -0
- package/ui/totals.css +96 -0
- package/ui/totals.mjs +94 -0
package/ui/totals.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { compactNumber } from './format.mjs'
|
|
2
|
+
import { esc, instAttrs, termButton } from './html.mjs'
|
|
3
|
+
import { FOOT_DRAW, FOOT_SIZE, binTimes } from './pictures.mjs'
|
|
4
|
+
import { linesHTML } from './rows.mjs'
|
|
5
|
+
import { URGENCY_ORDER, lampHTML, statusOf } from './status.mjs'
|
|
6
|
+
|
|
7
|
+
export function statusCounts(list) {
|
|
8
|
+
const counts = Object.fromEntries(URGENCY_ORDER.map((k) => [k, 0]))
|
|
9
|
+
for (const s of list) if (s.status in counts) counts[s.status]++
|
|
10
|
+
return counts
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function lampsHTML(counts, total, pressed = null) {
|
|
14
|
+
const lamps = URGENCY_ORDER.map((k) => {
|
|
15
|
+
const name = statusOf(k).name
|
|
16
|
+
const n = counts[k] || 0
|
|
17
|
+
return `<button type="button" class="lamp-btn" data-st="${k}" data-act="status" data-key="${k}" aria-pressed="${pressed === k}"${n ? '' : ' data-zero'} data-tip="${esc(`${name}: ${n}. Show only these.`)}" aria-label="${esc(`${name}: ${n}`)}">${lampHTML(k)}<b>${n}</b><span class="word">${esc(name)}</span></button>`
|
|
18
|
+
}).join('')
|
|
19
|
+
return `${lamps}<button type="button" class="lamp-btn total" data-act="status" data-key="" aria-pressed="false" data-tip="${esc(`${total} threads in total. Show them all.`)}" aria-label="${esc(`${total} threads in total`)}"><b>${total}</b><span class="word">${total === 1 ? 'Thread' : 'Threads'}</span></button>`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function footerTotals(list) {
|
|
23
|
+
let out = 0
|
|
24
|
+
let outKnown = false
|
|
25
|
+
let added = 0
|
|
26
|
+
let removed = 0
|
|
27
|
+
let linesKnown = false
|
|
28
|
+
for (const s of list) {
|
|
29
|
+
if (s.tokens && Number.isFinite(s.tokens.output)) {
|
|
30
|
+
out += s.tokens.output
|
|
31
|
+
outKnown = true
|
|
32
|
+
}
|
|
33
|
+
if (s.lines) {
|
|
34
|
+
added += s.lines.added || 0
|
|
35
|
+
removed += s.lines.removed || 0
|
|
36
|
+
linesKnown = true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const counts = statusCounts(list)
|
|
40
|
+
return { live: list.filter((s) => s.live).length, total: list.length, sts: URGENCY_ORDER.map((k) => counts[k]), out, outKnown, added, removed, linesKnown }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function totalHTML(k, term, spec, val, shown, label) {
|
|
44
|
+
const svg = spec ? FOOT_DRAW[spec.k](spec, FOOT_SIZE) : ''
|
|
45
|
+
const attrs = svg ? instAttrs(spec, val) : ''
|
|
46
|
+
return `<span class="tot" data-k="${k}">${svg ? `<span class="i-wrap" data-term="${term}" ${attrs}>${svg}</span>` : ''}<span class="tot-t"><b>${shown}</b>${termButton(term, label, attrs)}</span></span>`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function footerHTML(t, { short = false, bars = null } = {}) {
|
|
50
|
+
const out = compactNumber(t.out)
|
|
51
|
+
return [
|
|
52
|
+
totalHTML('live', 'liveAgents', { k: 'waffle', run: t.live, total: t.total, sts: t.sts }, `${t.live} of ${t.total} threads`, t.live, short ? 'Live' : 'Live agents'),
|
|
53
|
+
totalHTML('output', 'outputTotal', t.outKnown && { k: 'cols', v: t.out, base: 1000, ...(bars ? { bars } : {}) }, out, t.outKnown ? esc(out) : '—', short ? 'Output' : 'Output tokens'),
|
|
54
|
+
totalHTML('lines', 'linesTotal', t.linesKnown && { k: 'pie', a: t.added, r: t.removed }, `+${compactNumber(t.added)} −${compactNumber(t.removed)}`, t.linesKnown ? linesHTML({ added: t.added, removed: t.removed }) : '—', short ? 'Lines' : 'Lines changed'),
|
|
55
|
+
].join('')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const OUTPUT_WINDOW_MS = 60 * 60 * 1000
|
|
59
|
+
export const OUTPUT_MIN_SAMPLES = 3
|
|
60
|
+
export const OUTPUT_BINS = 14
|
|
61
|
+
|
|
62
|
+
export const emptyOutputLog = () => ({ since: null, last: new Map(), samples: [] })
|
|
63
|
+
|
|
64
|
+
export function recordOutput(log, sessions, now) {
|
|
65
|
+
const last = new Map()
|
|
66
|
+
const grew = []
|
|
67
|
+
for (const s of sessions) {
|
|
68
|
+
const out = s.tokens?.output
|
|
69
|
+
if (!Number.isFinite(out)) continue
|
|
70
|
+
last.set(s.id, out)
|
|
71
|
+
const before = log.last.get(s.id)
|
|
72
|
+
if (before != null && out > before) grew.push([s.id, out - before])
|
|
73
|
+
}
|
|
74
|
+
const samples = log.samples.filter((x) => now - x.at <= OUTPUT_WINDOW_MS)
|
|
75
|
+
if (grew.length) samples.push({ at: now, out: new Map(grew) })
|
|
76
|
+
return { since: log.since ?? now, last, samples }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function outputBars(log, ids, now, n = OUTPUT_BINS) {
|
|
80
|
+
if (log.since == null) return null
|
|
81
|
+
const shown = new Set(ids)
|
|
82
|
+
const times = []
|
|
83
|
+
const amounts = []
|
|
84
|
+
for (const x of log.samples) {
|
|
85
|
+
let v = 0
|
|
86
|
+
for (const [id, d] of x.out) if (shown.has(id)) v += d
|
|
87
|
+
if (!v) continue
|
|
88
|
+
times.push(x.at)
|
|
89
|
+
amounts.push(v)
|
|
90
|
+
}
|
|
91
|
+
if (times.length < OUTPUT_MIN_SAMPLES) return null
|
|
92
|
+
const from = Math.max(log.since, now - OUTPUT_WINDOW_MS)
|
|
93
|
+
return binTimes(times, from, Math.max(now, from + 1), amounts, n)
|
|
94
|
+
}
|