@ucsandman/legcli 0.15.0 → 0.16.0
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/CHANGELOG.md +54 -0
- package/README.md +5 -3
- package/docs/DEVIATIONS.md +1 -0
- package/docs/ERRORS.md +39 -0
- package/docs/adapters.md +5 -2
- package/docs/board-guide.md +33 -29
- package/docs/cli-contracts.md +14 -6
- package/docs/faq.md +4 -3
- package/docs/screenshots/board-400px.png +0 -0
- package/docs/screenshots/board-details-open.png +0 -0
- package/docs/screenshots/board-empty.png +0 -0
- package/docs/screenshots/board-handoff.png +0 -0
- package/docs/screenshots/capacity-drawer-1280.png +0 -0
- package/docs/screenshots/floor.png +0 -0
- package/docs/screenshots/new-card-dialog.png +0 -0
- package/docs/screenshots/settings-ladder-1280.png +0 -0
- package/docs/screenshots/terminals-1280.png +0 -0
- package/package.json +1 -1
- package/scripts/npm-publish-gate.mjs +4 -0
- package/scripts/release-notes.mjs +51 -0
- package/src/board/board.css +177 -55
- package/src/board/board.js +5 -2
- package/src/board/entry.js +4 -4
- package/src/board/favicon.svg +1 -1
- package/src/board/floor.html +2 -2
- package/src/board/floor.js +4 -1
- package/src/board/history.js +1 -1
- package/src/board/index.html +22 -8
- package/src/board/sessions.js +55 -14
- package/src/board/strip.js +1 -1
- package/src/hook.mjs +5 -4
- package/src/taps/claude-usage.mjs +5 -4
- package/src/taps/claude.mjs +34 -6
package/src/board/sessions.js
CHANGED
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
const alsoOpen = new Set()
|
|
51
51
|
const actionNotes = new Map()
|
|
52
52
|
const lastTone = new Map()
|
|
53
|
+
const rowShape = new Map()
|
|
53
54
|
const defaultEditor = { ladder: null, climb_back: 'next-handoff', may_spend: false, reserve: {}, dirty: false, saving: false, status: '', statusClass: '' }
|
|
54
55
|
|
|
55
56
|
// localStorage THROWS rather than answering null in a browser with site data
|
|
@@ -635,6 +636,19 @@
|
|
|
635
636
|
// `card 3e1c`: the tail of the card id, which is how a card is named on its
|
|
636
637
|
// own row and out loud.
|
|
637
638
|
function cardName(id) { return `card ${String(id).split('-').filter(Boolean).slice(-1)[0] || id}` }
|
|
639
|
+
// the terminal the verdict is about when a human is blocked: the one that
|
|
640
|
+
// has waited longest. verdictLines prints it; verdictTarget names its row.
|
|
641
|
+
function blockedOf(live) {
|
|
642
|
+
return live
|
|
643
|
+
.filter((s) => notifyWait(s) && s.waiting.type !== 'quota_auto_resume' && Number.isFinite(Date.parse(s.waiting.since)))
|
|
644
|
+
.sort((x, y) => Date.parse(x.waiting.since) - Date.parse(y.waiting.since))[0]
|
|
645
|
+
}
|
|
646
|
+
function verdictTarget(list, sessions) {
|
|
647
|
+
const accounts = (list || []).filter((a) => a && a.agent && !a.loading)
|
|
648
|
+
if (!accounts.length) return null
|
|
649
|
+
const b = blockedOf((sessions || []).filter((s) => s.active))
|
|
650
|
+
return b ? b.session_id : null
|
|
651
|
+
}
|
|
638
652
|
function verdictLines(list, sessions, cards) {
|
|
639
653
|
const accounts = (list || []).filter((a) => a && a.agent && !a.loading)
|
|
640
654
|
if (!accounts.length) return { line: 'Reading the logins.', sub: '' }
|
|
@@ -668,9 +682,7 @@
|
|
|
668
682
|
// the TYPE is checked and never the presence of `since`. `quota_auto_resume`
|
|
669
683
|
// is left out here too: nobody asked the human anything, Claude Code is
|
|
670
684
|
// holding its own turn, and the row says so in its own sentence.
|
|
671
|
-
const blocked = live
|
|
672
|
-
.filter((s) => notifyWait(s) && s.waiting.type !== 'quota_auto_resume' && Number.isFinite(Date.parse(s.waiting.since)))
|
|
673
|
-
.sort((x, y) => Date.parse(x.waiting.since) - Date.parse(y.waiting.since))[0]
|
|
685
|
+
const blocked = blockedOf(live)
|
|
674
686
|
if (blocked) {
|
|
675
687
|
const who = rowName(blocked)
|
|
676
688
|
const waited = spoken(Date.now() - Date.parse(blocked.waiting.since))
|
|
@@ -881,7 +893,23 @@
|
|
|
881
893
|
const { line, sub } = verdictLines(list, view ? view.sessions : [], view ? view.cards_waiting : null)
|
|
882
894
|
const h1 = document.getElementById('verdict-line')
|
|
883
895
|
const p = document.getElementById('verdict-sub')
|
|
884
|
-
|
|
896
|
+
const target = verdictTarget(list, view ? view.sessions : [])
|
|
897
|
+
if (h1) {
|
|
898
|
+
// the sentence names a row, so the sentence is the way to that row: a
|
|
899
|
+
// button in the h1's own clothes that scrolls to the row and hands it the
|
|
900
|
+
// keyboard (the prompt is the row's first control)
|
|
901
|
+
if (target && typeof h1.replaceChildren === 'function') {
|
|
902
|
+
const link = el('button', { type: 'button', class: 'verdict-link', title: 'go to this terminal' }, [line])
|
|
903
|
+
link.addEventListener('click', () => {
|
|
904
|
+
const row = document.querySelector(`#session-grid .term[data-session-id="${target}"]`)
|
|
905
|
+
if (!row) return
|
|
906
|
+
if (typeof row.scrollIntoView === 'function') row.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
907
|
+
const first = row.querySelector('.panel-prompt, .btn')
|
|
908
|
+
if (first && typeof first.focus === 'function') first.focus({ preventScroll: true })
|
|
909
|
+
})
|
|
910
|
+
h1.replaceChildren(link)
|
|
911
|
+
} else h1.textContent = line
|
|
912
|
+
}
|
|
885
913
|
if (p) p.textContent = sub
|
|
886
914
|
// the strip is the only usage on screen until the reader opens the drawer
|
|
887
915
|
capacityStrip(list)
|
|
@@ -1717,7 +1745,14 @@
|
|
|
1717
1745
|
const urgent = needsYou(s, notes)
|
|
1718
1746
|
// the article is named so the accessibility tree does not hand the reader
|
|
1719
1747
|
// three identical triples of Land / Hand off now / Details / End
|
|
1720
|
-
|
|
1748
|
+
// what the row says, in one string: when it differs from the last render
|
|
1749
|
+
// the row is lit for a moment (board.css .term.is-changed), the one answer
|
|
1750
|
+
// the board gives to what moved while the reader was away
|
|
1751
|
+
const shape = `${s.status}|${urgent}|${notes[0] ? notes[0].cat : ''}|${promptText(s.task)}`
|
|
1752
|
+
const was = rowShape.get(s.session_id)
|
|
1753
|
+
rowShape.set(s.session_id, shape)
|
|
1754
|
+
const changed = was !== undefined && was !== shape
|
|
1755
|
+
const term = el('article', { class: `term${urgent ? ' is-urgent' : ''}${changed ? ' is-changed' : ''}`, 'data-session-id': s.session_id, 'aria-label': `${s.agent} ${tail(s.session_id)}` })
|
|
1721
1756
|
const row = el('div', { class: 'term-row' })
|
|
1722
1757
|
const body = el('div', { class: 'term-body' })
|
|
1723
1758
|
|
|
@@ -1733,7 +1768,7 @@
|
|
|
1733
1768
|
// which model actually answered, and whether it has gone quiet. The model
|
|
1734
1769
|
// token is the agent and the model it resolved to, never a default.
|
|
1735
1770
|
for (const t of registerTokens(s)) {
|
|
1736
|
-
if (t.kind === 'model') register.appendChild(el('span', { class: `term-model chip-id-${idOf(s.agent)}` }, [t.text]))
|
|
1771
|
+
if (t.kind === 'model') register.appendChild(el('span', { class: `term-model chip-id-${idOf(s.agent)}` }, [el('span', { class: `dot id-${idOf(s.agent)}`, 'aria-hidden': 'true' }), t.text]))
|
|
1737
1772
|
else register.appendChild(el('span', { class: `term-${t.kind}` }, [t.text]))
|
|
1738
1773
|
}
|
|
1739
1774
|
if (s.account !== 'default') register.appendChild(el('span', { class: 'chip' }, [s.account]))
|
|
@@ -1822,8 +1857,11 @@
|
|
|
1822
1857
|
row.appendChild(body)
|
|
1823
1858
|
|
|
1824
1859
|
// elapsed and the short id, right-aligned and small: the two facts you scan
|
|
1825
|
-
// down the column rather than read
|
|
1826
|
-
|
|
1860
|
+
// down the column rather than read. They sit over the actions in one side
|
|
1861
|
+
// column (board.css .term-side); the narrow breakpoint dissolves it.
|
|
1862
|
+
const side = el('div', { class: 'term-side' })
|
|
1863
|
+
row.appendChild(side)
|
|
1864
|
+
side.appendChild(el('div', { class: 'term-clock' }, [
|
|
1827
1865
|
el('span', { class: 'term-when elapsed', 'data-elapsed-from': String(Date.parse(s.started_at) || 0), 'data-elapsed-format': 'compact', title: `started ${new Date(s.started_at).toLocaleString()}` }, [ago(s.elapsed_ms)]),
|
|
1828
1866
|
// the tail is `codex-99ab`, printed immediately after the word `codex`:
|
|
1829
1867
|
// the prefix is the agent name twice, and it is the half that squeezed
|
|
@@ -1859,7 +1897,7 @@
|
|
|
1859
1897
|
q.addEventListener('click', () => act(s.session_id, 'request-handoff', q))
|
|
1860
1898
|
actions.appendChild(q)
|
|
1861
1899
|
}
|
|
1862
|
-
|
|
1900
|
+
side.appendChild(actions)
|
|
1863
1901
|
term.appendChild(row)
|
|
1864
1902
|
return term
|
|
1865
1903
|
}
|
|
@@ -1924,7 +1962,7 @@
|
|
|
1924
1962
|
actions.appendChild(land)
|
|
1925
1963
|
}
|
|
1926
1964
|
if (s.active) {
|
|
1927
|
-
const h = el('button', { type: 'button', class:
|
|
1965
|
+
const h = el('button', { type: 'button', class: 'btn btn-secondary', title: 'save the bundle, stop this agent, start the next option in the same terminal', 'data-focus-key': `handoff:${s.session_id}` }, ['Hand off now'])
|
|
1928
1966
|
// the same confirm row End and the picker already use. This stops the
|
|
1929
1967
|
// current turn of a working agent, and the `h` key presses this button:
|
|
1930
1968
|
// an action that costs a turn asks first, whichever hand pressed it.
|
|
@@ -1961,7 +1999,7 @@
|
|
|
1961
1999
|
no.addEventListener('click', () => act(s.session_id, `requests/${encodeURIComponent(r.by)}/dismiss`, no))
|
|
1962
2000
|
actions.append(ok, no)
|
|
1963
2001
|
}
|
|
1964
|
-
|
|
2002
|
+
side.appendChild(actions)
|
|
1965
2003
|
term.appendChild(row)
|
|
1966
2004
|
|
|
1967
2005
|
// Why can't I land expander or fallback blocker message
|
|
@@ -2573,7 +2611,7 @@
|
|
|
2573
2611
|
// favicon are the only surface a browser gives a tab nobody is looking at.
|
|
2574
2612
|
// favicon.svg itself is never touched; the dot is a variant drawn inline.
|
|
2575
2613
|
const FAVICON = '/favicon.svg'
|
|
2576
|
-
const FAVICON_DOT = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
2614
|
+
const FAVICON_DOT = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#F3F4F7"/><path d="M8 5.5v8.5l4 4h5" stroke="#0E1012" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/><circle cx="19" cy="5" r="5" fill="#E64343"/></svg>')}`
|
|
2577
2615
|
function titleBadge(n) {
|
|
2578
2616
|
const title = n > 0 ? `(${n}) Leg` : 'Leg'
|
|
2579
2617
|
if (document.title !== title) document.title = title
|
|
@@ -2792,7 +2830,7 @@
|
|
|
2792
2830
|
const repos = [...new Set(finished.map((s) => s.repo_name).filter(Boolean))]
|
|
2793
2831
|
if (repos.length) meta.textContent += `, in ${repos.slice(0, 3).join(', ')}${repos.length > 3 ? ` and ${repos.length - 3} more` : ''}`
|
|
2794
2832
|
const btn = el('button', { type: 'button', class: 'btn btn-secondary', 'aria-expanded': finishedOpen ? 'true' : 'false', 'aria-controls': 'finished-drawer', 'data-focus-key': 'finished-toggle' },
|
|
2795
|
-
[finishedOpen ?
|
|
2833
|
+
[finishedOpen ? 'Hide' : 'View'])
|
|
2796
2834
|
btn.addEventListener('click', () => { finishedOpen = !finishedOpen; renderSessions(view) })
|
|
2797
2835
|
slot.appendChild(btn)
|
|
2798
2836
|
panel.hidden = !finishedOpen
|
|
@@ -2842,6 +2880,9 @@
|
|
|
2842
2880
|
orderDivergedAt = same ? 0 : (orderDivergedAt || Date.now())
|
|
2843
2881
|
const empty = document.querySelector('.region-terminals .empty-line')
|
|
2844
2882
|
if (empty) empty.hidden = list.some((s) => s.active || needsYou(s, notesOf.get(s.session_id) || []))
|
|
2883
|
+
// the first-run panel is the same fact in a lit panel (index.html #first-run)
|
|
2884
|
+
const firstRun = document.getElementById('first-run')
|
|
2885
|
+
if (firstRun) firstRun.hidden = list.length > 0
|
|
2845
2886
|
|
|
2846
2887
|
// A terminal that has ended or been lost is history, and history does not
|
|
2847
2888
|
// belong in the panel that shows what is live. It moves to the ledger.
|
|
@@ -2999,7 +3040,7 @@
|
|
|
2999
3040
|
// depends on it. board-updates.test.mjs uses the same pattern in board.js.
|
|
3000
3041
|
if (typeof module !== 'undefined') {
|
|
3001
3042
|
module.exports = {
|
|
3002
|
-
verdictLines, VERDICT_CH, SUB_CH, WARN_PCT, bindingOf, capFigure, capToken, shareClause, headline,
|
|
3043
|
+
verdictLines, verdictTarget, VERDICT_CH, SUB_CH, WARN_PCT, bindingOf, capFigure, capToken, shareClause, headline,
|
|
3003
3044
|
rankedNotes, needsYou, registerTokens, capacityPhrase, capacityNote, waitingNote, notifyWait, resetWait,
|
|
3004
3045
|
KEY_BUTTONS, KEY_MOVES,
|
|
3005
3046
|
// D14: the ring is state, not a pure function, so test/board-keyboard
|
package/src/board/strip.js
CHANGED
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
const drawer = document.getElementById('capacity-drawer')
|
|
141
141
|
if (btn) {
|
|
142
142
|
btn.setAttribute('aria-expanded', capacityOpen ? 'true' : 'false')
|
|
143
|
-
btn.textContent = capacityOpen ? 'Hide capacity and models' : 'Capacity and models
|
|
143
|
+
btn.textContent = capacityOpen ? 'Hide capacity and models' : 'Capacity and models'
|
|
144
144
|
}
|
|
145
145
|
if (drawer) drawer.hidden = !capacityOpen
|
|
146
146
|
}
|
package/src/hook.mjs
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// node hook.mjs claude-hook --session <id>
|
|
7
7
|
// node hook.mjs claude-statusline --session <id>
|
|
8
8
|
// The status line entry records rate_limits when a Claude Code build runs it
|
|
9
|
-
// (2.1.268
|
|
9
|
+
// (2.1.268 and 2.1.278 did not on this machine; see src/taps/claude-usage.mjs),
|
|
10
|
+
// prints the user's own status line first, then one Leg line.
|
|
10
11
|
import { appendFileSync } from 'node:fs'
|
|
11
12
|
import { join } from 'node:path'
|
|
12
13
|
import { handleHook, handleStatusline, terminalSequenceFor } from './taps/claude.mjs'
|
|
@@ -47,9 +48,9 @@ try {
|
|
|
47
48
|
const seq = terminalSequenceFor(payload)
|
|
48
49
|
if (seq) process.stdout.write(JSON.stringify({ terminalSequence: seq }) + '\n')
|
|
49
50
|
} else if (kind === 'claude-statusline') {
|
|
50
|
-
const { text } = handleStatusline(sessionId, payload)
|
|
51
|
-
try { appendFileSync(join(sessionDir(sessionId), 'hook.log'), `${new Date().toISOString()} statusline rate_limits=${JSON.stringify(payload.rate_limits ?? null)}\n`) } catch {}
|
|
52
|
-
process.stdout.write(text + '\n')
|
|
51
|
+
const { text, user } = handleStatusline(sessionId, payload, raw)
|
|
52
|
+
try { appendFileSync(join(sessionDir(sessionId), 'hook.log'), `${new Date().toISOString()} statusline rate_limits=${JSON.stringify(payload.rate_limits ?? null)} user_rows=${user ? user.split('\n').length : 0}\n`) } catch {}
|
|
53
|
+
process.stdout.write((user ? user + '\n' : '') + text + '\n')
|
|
53
54
|
}
|
|
54
55
|
} catch {}
|
|
55
56
|
process.exit(0)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// claude usage — the 5h / 7d percentages for a Claude Code login, from the
|
|
2
2
|
// same endpoint Claude Code's own /usage and built-in status line read.
|
|
3
|
-
// Why not the status line: Claude Code 2.1.268
|
|
4
|
-
// line and
|
|
5
|
-
// or a project settings file (verified 2026-09-11
|
|
6
|
-
// both levels; hooks from the same
|
|
3
|
+
// Why not the status line: Claude Code 2.1.268 and 2.1.278 render their
|
|
4
|
+
// built-in status line and do not run a custom `statusLine` command passed
|
|
5
|
+
// via --settings or a project settings file (verified 2026-09-11 and
|
|
6
|
+
// 2026-09-19 with an `echo` command at both levels; hooks from the same
|
|
7
|
+
// --settings file do run). So Leg asks the
|
|
7
8
|
// usage endpoint directly with the OAuth token Claude Code stored at login.
|
|
8
9
|
// The token is read by this process only, sent only to api.anthropic.com,
|
|
9
10
|
// and never written anywhere (the ledger scrubs bearer tokens regardless).
|
package/src/taps/claude.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// claude tap — how `leg claude` sees inside a normal interactive Claude Code.
|
|
2
2
|
// Nothing in ~/.claude is edited: the session gets one extra settings file via
|
|
3
3
|
// `--settings` (hooks merge with the user's; statusLine is the only key that
|
|
4
|
-
// replaces, so Leg's status
|
|
4
|
+
// replaces, so Leg's status-line hook runs the user's own command with the
|
|
5
|
+
// same stdin and prints its rows above Leg's one line).
|
|
5
6
|
// Sources: code.claude.com/docs/en/hooks (StopFailure `error: rate_limit`),
|
|
6
7
|
// docs/en/statusline (rate_limits.five_hour/seven_day used_percentage,
|
|
7
8
|
// resets_at), docs/en/settings (`--settings` sits above user settings).
|
|
8
9
|
import { existsSync, readFileSync, openSync, closeSync, fstatSync, readSync } from 'node:fs'
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
9
11
|
import { join, dirname, resolve } from 'node:path'
|
|
10
12
|
import { fileURLToPath } from 'node:url'
|
|
11
13
|
import { sessionDir, updateSession, appendEvent, readSession, workRoot } from '../sessions.mjs'
|
|
@@ -64,12 +66,37 @@ export function settingsFor(sessionId, { statusLine = null } = {}) {
|
|
|
64
66
|
return settings
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
export function writeSettings(sessionId, opts) {
|
|
69
|
+
export function writeSettings(sessionId, opts = {}) {
|
|
68
70
|
const file = join(sessionDir(sessionId), 'claude-settings.json')
|
|
69
71
|
writeJsonAtomic(file, settingsFor(sessionId, opts))
|
|
72
|
+
// the hook process reads this back to run the user's command (see userStatusLineText)
|
|
73
|
+
if (opts.statusLine?.command) updateSession(sessionId, { user_statusline: { command: opts.statusLine.command } })
|
|
70
74
|
return file
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
// The shell Claude Code itself uses for a status-line command: /bin/sh, or on
|
|
78
|
+
// Windows Git Bash when installed, else PowerShell (docs/en/statusline,
|
|
79
|
+
// "Windows configuration").
|
|
80
|
+
function statusLineShell(command) {
|
|
81
|
+
if (process.platform !== 'win32') return { file: '/bin/sh', args: ['-c', command] }
|
|
82
|
+
const bash = process.env.CLAUDE_CODE_GIT_BASH_PATH || 'C:\\Program Files\\Git\\bin\\bash.exe'
|
|
83
|
+
if (existsSync(bash)) return { file: bash, args: ['-c', command] }
|
|
84
|
+
return { file: 'powershell', args: ['-NoProfile', '-Command', command] }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The user's own status line, rendered: their command gets the same JSON on
|
|
88
|
+
// stdin Claude Code handed Leg, and whatever it prints goes above Leg's row.
|
|
89
|
+
// Never throws; an absent, slow (3 s) or broken command yields ''.
|
|
90
|
+
export function userStatusLineText(session, raw) {
|
|
91
|
+
const command = session?.user_statusline?.command
|
|
92
|
+
if (!command) return ''
|
|
93
|
+
try {
|
|
94
|
+
const { file, args } = statusLineShell(command)
|
|
95
|
+
const r = spawnSync(file, args, { input: raw ?? '', encoding: 'utf8', timeout: 3000, windowsHide: true })
|
|
96
|
+
return String(r.stdout ?? '').replace(/\s+$/, '')
|
|
97
|
+
} catch { return '' }
|
|
98
|
+
}
|
|
99
|
+
|
|
73
100
|
function textOf(content) {
|
|
74
101
|
if (typeof content === 'string') return content
|
|
75
102
|
if (Array.isArray(content)) return content.filter((c) => c?.type === 'text' && c.text).map((c) => c.text).join('\n')
|
|
@@ -288,10 +315,11 @@ export function relTo(root, file) {
|
|
|
288
315
|
return r && a.toLowerCase().startsWith(r.toLowerCase() + '/') ? a.slice(r.length + 1) : a
|
|
289
316
|
}
|
|
290
317
|
|
|
291
|
-
// Status line: record the limits and print. Returns
|
|
292
|
-
|
|
318
|
+
// Status line: record the limits and print. Returns Leg's row as `text` and
|
|
319
|
+
// the user's own status line (from `raw`, the stdin JSON) as `user`.
|
|
320
|
+
export function handleStatusline(sessionId, p, raw = '') {
|
|
293
321
|
const s = readSession(sessionId)
|
|
294
|
-
if (!s) return { text: '', limits: null }
|
|
322
|
+
if (!s) return { text: '', user: '', limits: null }
|
|
295
323
|
const limits = limitsFrom(p.rate_limits)
|
|
296
324
|
if (limits) recordUsage('claude', s.account, limits, 'claude statusline')
|
|
297
325
|
const hot = limits ? [['5h', limits.five_hour], ['7d', limits.seven_day]].filter(([, w]) => w).sort((a, b) => b[1].pct - a[1].pct)[0] : null
|
|
@@ -312,5 +340,5 @@ export function handleStatusline(sessionId, p) {
|
|
|
312
340
|
const next = s.chain?.[0] ? `${s.chain[0].agent}${s.chain[0].account !== 'default' ? '/' + s.chain[0].account : ''}` : 'nothing'
|
|
313
341
|
const pct = limits ? ` 5h ${limits.five_hour ? Math.round(limits.five_hour.pct) + '%' : '-'} · 7d ${limits.seven_day ? Math.round(limits.seven_day.pct) + '%' : '-'}` : ''
|
|
314
342
|
const text = warn ? `⚠ leg: ${hot[0]} at ${Math.round(hot[1].pct)}% → next ${next}${pct}` : `leg ·${pct || ' limits pending'} · next ${next} · board ${s.board_url ?? ''}`
|
|
315
|
-
return { text, limits, warn }
|
|
343
|
+
return { text, user: userStatusLineText(s, raw), limits, warn }
|
|
316
344
|
}
|