import { atom, read, update } from 'claude-code' import type { EngineInterface, Register, SessionContextUsage, SessionRateLimit } from 'claude-code' import type { Mode, Sys, Usage, Window } from '../types' import { fit, cpuPercent, parseCpu, parseDf, parseGit, parseMem, parseOffset } from './format' import type { CpuSample } from './format' const usageAtom = atom({ plugin: 'jsc-mod', key: 'usage' } as const, {}) const gitAtom = atom({ plugin: 'jsc-mod', key: 'git' } as const, null) const sysAtom = atom({ plugin: 'jsc-mod', key: 'sys' } as const, {}) const modeAtom = atom({ plugin: 'jsc-mod', key: 'mode' } as const, 'auto') const alertedAtom = atom({ plugin: 'jsc-mod', key: 'alerted' } as const, []) const effortAtom = atom({ plugin: 'jsc-mod', key: 'effort' } as const, null) const SYS_MS = 3000 const GIT_MS = 15_000 const DISK_EVERY = 10 // 每 10 次系統取樣(30 秒)讀一次磁碟 const ALERT_AT = 90 const GIT_TOOLS = new Set(['Bash', 'Edit', 'Write', 'NotebookEdit']) const LABEL: Record = { five_hour: '5 小時', seven_day: '7 天' } // 引擎要等第一次 API 回應才有額度與 ctx;先前的額度存在 $.store,開新對話時先顯示,ctx 先當作 0 function toUsage(context: SessionContextUsage, rateLimits: SessionRateLimit[]): Usage { const win = (kind: string) => { const r = rateLimits.find(x => x.kind === kind) return r && { percent: r.percentUsed, resetsAt: r.resetsAt } } return { fiveHour: win('five_hour'), sevenDay: win('seven_day'), ctxTokens: context.tokens ?? 0, ctxWindow: context.window, ctxPercent: context.percent ?? 0, } } // 已過重置時間的視窗用量已歸零,不再沿用舊值 const live = (w: Window | undefined, now: number) => w && (!w.resetsAt || Date.parse(w.resetsAt) > now) ? w : undefined async function restoreRates($: EngineInterface, usage: Usage): Promise { const saved = (await $.store.get('rates')) as Pick | undefined if (!saved) return usage const now = await $.clock.now() return { ...usage, fiveHour: usage.fiveHour ?? live(saved.fiveHour, now), sevenDay: usage.sevenDay ?? live(saved.sevenDay, now), } } let home: string | undefined let tzOffsetMin = 0 let prevCpu: CpuSample | null = null let ticks = 0 async function refreshGit($: EngineInterface) { try { const r = await $.process.run(['git', 'status', '--porcelain=v2', '-b'], { timeoutMs: 5000 }) const git = r.exitCode === 0 ? parseGit(r.stdout) : null await update($, gitAtom, () => git) } catch { await update($, gitAtom, () => null) } } async function refreshSys($: EngineInterface) { const next: Sys = { ...(await read($, sysAtom)) } try { const cur = parseCpu(await $.fs.read('/proc/stat')) if (cur) { if (prevCpu) next.cpu = cpuPercent(prevCpu, cur) ?? next.cpu prevCpu = cur } } catch {} try { const mem = parseMem(await $.fs.read('/proc/meminfo')) if (mem) [next.memUsed, next.memTotal] = [mem.used, mem.total] } catch {} if (ticks++ % DISK_EVERY === 0) { try { const r = await $.process.run(['df', '-P', await $.session.cwd()], { timeoutMs: 5000 }) if (r.exitCode === 0) next.disk = parseDf(r.stdout) } catch {} } await update($, sysAtom, () => next) } // 每個額度視窗(以重置時間區分)只提醒一次 async function alert($: EngineInterface, rateLimits: SessionRateLimit[]) { const done = await read($, alertedAtom) for (const r of rateLimits) { const id = `${r.kind}@${r.resetsAt ?? ''}` if (r.percentUsed < ALERT_AT || done.includes(id)) continue await update($, alertedAtom, list => [...list, id].slice(-20)) const label = LABEL[r.kind] ?? r.kind $.ui.toast(`${label}額度已用 ${Math.round(r.percentUsed)}%`) } } async function runHud($: EngineInterface, args: string) { const arg = args.trim() const mode: Mode = arg === 'off' ? 'off' : arg === 'compact' ? 'compact' : 'auto' await update($, modeAtom, () => mode) const said: Record = { auto: '狀態列已開啟。', off: '狀態列已隱藏。', compact: '狀態列改用極窄版。' } return { text: said[mode] } } export const register: Register = on => { on('session.start', async ($, e, next) => { await $.command.register({ name: 'statusline', description: '切換輸入框上方的狀態列', argumentHint: 'on | off | compact', }) home = await $.env.get('HOME') try { const r = await $.process.run(['date', '+%z'], { timeoutMs: 3000 }) tzOffsetMin = parseOffset(r.stdout) } catch {} const u = await $.session.usage() const usage = await restoreRates($, toUsage(u.context, u.rateLimits)) await update($, usageAtom, () => usage) void refreshGit($) // CPU 要兩次取樣才算得出來,開場先隔 0.5 秒補一次,免得等到 3 秒後才出現 void refreshSys($).then(() => $.clock.after(500, () => void refreshSys($))) $.clock.every(SYS_MS, () => void refreshSys($)) $.clock.every(GIT_MS, () => void refreshGit($)) return next(e) }) on('session.measure', async ($, e, next) => { await update($, usageAtom, () => toUsage(e.context, e.rateLimits)) if (e.rateLimits.length) { const w = (kind: string) => e.rateLimits.find(x => x.kind === kind) const pick = (r: SessionRateLimit | undefined) => r && { percent: r.percentUsed, resetsAt: r.resetsAt } await $.store.set('rates', { fiveHour: pick(w('five_hour')), sevenDay: pick(w('seven_day')) }) } if (e.changed.includes('rateLimits')) await alert($, e.rateLimits) return next(e) }) // 思考等級只在送出模型請求時看得到;只記主對話的,不記 subagent on('turn.step', async function* ($, e, next) { if (!e.agentId) { const effort = e.effort === undefined ? null : String(e.effort) if (effort !== (await read($, effortAtom))) await update($, effortAtom, () => effort) } return yield* next(e) }) on('tool.call', async ($, e, next) => { const ran = await next(e) if (GIT_TOOLS.has(e.tool)) void refreshGit($) return ran }) on('command.run', { command: 'statusline' }, ($, e) => runHud($, e.args)) on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => { const mode = await read($, modeAtom) const rest = await next(e) if (e.props.hasSurvey || mode === 'off') return rest const data = { model: await $.session.model(), cwd: await $.session.cwd(), home, git: await read($, gitAtom), usage: await read($, usageAtom), sys: await read($, sysAtom), tzOffsetMin, effort: await read($, effortAtom), } const line = fit(data, e.props.bodyColumns, mode === 'compact') const { Box, Text } = $.ui.resolve(e) // 引擎在 band 與輸入框之間已留一行空白,上方補一行讓上下間距相同 return ( {line.map(s => ( {s.text} ))} {rest} ) }) }