refactor: jsc-statusbar 改名為 jsc-mod,指令改為 /statusline,版本重設為 0.0.1

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
root
2026-10-14 04:45:00 +00:00
co-authored by Claude Sonnet 5.5
parent 5701b8b8e5
commit 88a3a3aff2
12 changed files with 49 additions and 49 deletions
+198
View File
@@ -0,0 +1,198 @@
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<string, string> = { 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<Usage> {
const saved = (await $.store.get('rates')) as Pick<Usage, 'fiveHour' | 'sevenDay'> | 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<Mode, string> = { 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 (
<Box marginTop={1} flexDirection="column">
<Text wrap="truncate">
{line.map(s => (
<Text color={s.color} dimColor={s.dim} bold={s.bold}>
{s.text}
</Text>
))}
</Text>
{rest}
</Box>
)
})
}