feat: initial commit for jsc-marketplace with hud plugin and MIT license

This commit is contained in:
Jeffery
2026-10-12 11:29:31 +08:00
commit a0b4008fd5
11 changed files with 626 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"name": "jsc-marketplace",
"description": "JSC 自用 Claude Code mods 的 marketplace。",
"owner": {
"name": "JSC"
},
"plugins": [
{
"name": "hud",
"source": "./plugins/hud",
"description": "輸入框上方單行 HUD:模型、目錄、分支、5h/7d 額度與重置、context、CPU/記憶體/磁碟"
}
]
}
+1
View File
@@ -0,0 +1 @@
**/.claude-plugin/types/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Jeffery (jiantw83)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+27
View File
@@ -0,0 +1,27 @@
# jsc-marketplace
JSC 自用 Claude Code mods。
| Mod | 說明 |
| --- | --- |
| [hud](plugins/hud) | 輸入框上方單行 HUD:模型、目錄、分支、5h/7d 額度與重置、context、CPU/記憶體/磁碟 |
## 安裝
本機資料夾:
```bash
claude plugin marketplace add /root/jsc-marketplace
claude plugin install hud@jsc-marketplace
```
或透過 Gitea 遠端:
```bash
claude plugin marketplace add https://gitea.jsc.idv.tw/jiantw83/jsc-marketplace.git
claude plugin install hud@jsc-marketplace
```
## 授權
[MIT License](LICENSE)
+10
View File
@@ -0,0 +1,10 @@
{
"name": "hud",
"version": "0.1.0",
"description": "輸入框上方單行 HUD:模型、目錄、分支、5h/7d 額度與重置、context、CPU/記憶體/磁碟",
"license": "MIT",
"types": "./types/index.d.ts",
"author": {
"name": "JSC"
}
}
+234
View File
@@ -0,0 +1,234 @@
import type { Git, Sys, Usage, Window } from '../types'
export type Seg = { text: string; color?: string; dim?: boolean; bold?: boolean }
export type HudData = {
model: string
cwd: string
home?: string
git: Git | null
usage: Usage
sys: Sys
tzOffsetMin: number
effort?: string | null
}
// 收合層級:0 完整 → 1 去進度條 → 2 目錄只留末段 → 3 去 context 用量 → 4 去磁碟 → 5 去重置時間 → 6 極窄
export const MAX_LEVEL = 6
export const levelColor = (p: number) => (p >= 85 ? 'error' : p >= 60 ? 'warning' : 'success')
export function bar(p: number, cells = 5): string {
const n = Math.max(0, Math.min(cells, Math.round((p / 100) * cells)))
return '▰'.repeat(n) + '▱'.repeat(cells - n)
}
export function prettyModel(raw: string): string {
const s = raw.replace(/\[1m\]/i, '').trim()
const m = /claude-(opus|sonnet|haiku|fable)-(\d+)(?:-(\d+))?/i.exec(s)
const fam = m?.[1]
if (!m || !fam) return s
const name = fam.charAt(0).toUpperCase() + fam.slice(1).toLowerCase()
return m[3] ? `${name} ${m[2]}.${m[3]}` : `${name} ${m[2]}`
}
// 模型等級:冷色空心 → 暖色實心,避開表示用量門檻的綠/黃/紅
const TIERS: Record<string, { glyph: string; color?: string }> = {
haiku: { glyph: '○', color: '#7f9bb3' },
sonnet: { glyph: '◇', color: '#4f9ee8' },
opus: { glyph: '◆', color: 'claude' },
fable: { glyph: '✦', color: '#a77bf0' },
}
export function modelStyle(raw: string): { glyph: string; color?: string } {
const fam = /(haiku|sonnet|opus|fable)/i.exec(raw)?.[1]?.toLowerCase()
return (fam && TIERS[fam]) || { glyph: '◆' }
}
export function tokens(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
if (n >= 1000) return `${Math.round(n / 1000)}k`
return String(n)
}
const pad = (n: number) => String(n).padStart(2, '0')
const shift = (iso: string, off: number) => new Date(Date.parse(iso) + off * 60_000)
export const clock = (iso: string, off: number) => {
const d = shift(iso, off)
return `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`
}
export const day = (iso: string, off: number) => {
const d = shift(iso, off)
return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`
}
// `+0800` → 480
export function parseOffset(z: string): number {
const m = /^([+-])(\d{2})(\d{2})$/.exec(z.trim())
if (!m) return 0
const v = Number(m[2]) * 60 + Number(m[3])
return m[1] === '-' ? -v : v
}
export function shortDir(cwd: string, home: string | undefined, lastOnly: boolean): string {
const p = home && (cwd === home || cwd.startsWith(home + '/')) ? '~' + cwd.slice(home.length) : cwd
if (!lastOnly) return p
const last = p.split('/').filter(Boolean).pop()
return last && last !== p ? `…/${last}` : p
}
const WIDE = /[ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/
export function width(s: string): number {
let w = 0
for (const ch of s) w += WIDE.test(ch) ? 2 : 1
return w
}
export const lineWidth = (segs: Seg[]) => segs.reduce((w, s) => w + width(s.text), 0)
function rate(label: string, w: Window | undefined, level: number, reset: (iso: string) => string): Seg[] {
if (!w) return [{ text: `${label} —`, dim: true }]
const c = levelColor(w.percent)
const out: Seg[] = [{ text: `${label} `, dim: true }]
if (level < 1) out.push({ text: `${bar(w.percent)} `, color: c })
out.push({ text: `${Math.round(w.percent)}%`, color: c })
if (level < 5 && w.resetsAt) out.push({ text: ` ↻${reset(w.resetsAt)}`, dim: true })
return out
}
function full(d: HudData, level: number): Seg[] {
const groups: Seg[][] = []
const ms = modelStyle(d.model)
const model: Seg[] = [{ text: `${ms.glyph} ${prettyModel(d.model)}`, color: ms.color, bold: true }]
if (d.effort && level < 5) model.push({ text: ` ·${d.effort}`, dim: true })
groups.push(model)
groups.push([{ text: shortDir(d.cwd, d.home, level >= 2), color: 'suggestion' }])
if (d.git) {
const g: Seg[] = [{ text: '⎇ ', dim: true }, { text: d.git.branch, bold: true }]
if (d.git.dirty) g.push({ text: ` ±${d.git.dirty}`, color: 'warning' })
if (d.git.ahead) g.push({ text: ` ↑${d.git.ahead}`, dim: true })
if (d.git.behind) g.push({ text: ` ↓${d.git.behind}`, dim: true })
groups.push(g)
}
groups.push(rate('5h', d.usage.fiveHour, level, iso => clock(iso, d.tzOffsetMin)))
groups.push(rate('7d', d.usage.sevenDay, level, iso => day(iso, d.tzOffsetMin)))
const u = d.usage
if (u.ctxPercent === undefined) {
groups.push([{ text: 'ctx —', dim: true }])
} else {
const c: Seg[] = [{ text: 'ctx ', dim: true }, { text: `${u.ctxPercent}%`, color: levelColor(u.ctxPercent) }]
if (level < 3 && u.ctxTokens !== undefined && u.ctxWindow)
c.push({ text: ` ${tokens(u.ctxTokens)}/${tokens(u.ctxWindow)}`, dim: true })
groups.push(c)
}
const s = d.sys
const sys: Seg[] = []
const add = (label: string, p: number | undefined) => {
if (p === undefined) return
if (sys.length) sys.push({ text: ' ' })
sys.push({ text: `${label} `, dim: true }, { text: `${Math.round(p)}%`, color: levelColor(p) })
}
add('CPU', s.cpu)
add('MEM', s.memUsed !== undefined && s.memTotal ? (s.memUsed / s.memTotal) * 100 : undefined)
if (level < 4) add('DSK', s.disk)
if (sys.length) groups.push(sys)
const line: Seg[] = []
groups.forEach((g, i) => {
if (i) line.push({ text: ' │ ', dim: true })
line.push(...g)
})
return line
}
function ultra(d: HudData): Seg[] {
const u = d.usage
const ms = modelStyle(d.model)
const out: Seg[] = [{ text: `${ms.glyph} ${prettyModel(d.model).split(' ')[0]}`, color: ms.color, bold: true }]
if (d.git) out.push({ text: ' ⎇', dim: true }, { text: d.git.branch, bold: true })
const num = (label: string, p: number | undefined) => {
out.push({ text: ` ${label}`, dim: true })
out.push(p === undefined ? { text: '—', dim: true } : { text: String(Math.round(p)), color: levelColor(p) })
}
num('5h', u.fiveHour?.percent)
num('7d', u.sevenDay?.percent)
num('ctx', u.ctxPercent)
if (d.sys.cpu !== undefined) num('cpu', d.sys.cpu)
return out
}
export function build(d: HudData, level: number): Seg[] {
return level >= MAX_LEVEL ? ultra(d) : full(d, level)
}
// 由完整版往下收合,回傳第一個放得進 columns 的版本
export function fit(d: HudData, columns: number, compact = false): Seg[] {
if (compact) return ultra(d)
for (let level = 0; level < MAX_LEVEL; level++) {
const line = full(d, level)
if (lineWidth(line) <= columns) return line
}
return ultra(d)
}
// git status --porcelain=v2 -b
export function parseGit(out: string): Git | null {
let head = ''
let oid = ''
let ahead = 0
let behind = 0
let dirty = 0
for (const line of out.split('\n')) {
if (!line) continue
if (line.startsWith('# branch.head ')) head = line.slice(14)
else if (line.startsWith('# branch.oid ')) oid = line.slice(13)
else if (line.startsWith('# branch.ab ')) {
const m = /\+(\d+) -(\d+)/.exec(line)
if (m) [ahead, behind] = [Number(m[1]), Number(m[2])]
} else if (!line.startsWith('#')) dirty++
}
if (!head) return null
const branch = head === '(detached)' ? oid.slice(0, 7) || 'detached' : head
return { branch, dirty, ahead, behind }
}
export type CpuSample = { idle: number; total: number }
export function parseCpu(stat: string): CpuSample | null {
const line = stat.split('\n').find(l => l.startsWith('cpu '))
if (!line) return null
const v = line.trim().split(/\s+/).slice(1).map(Number)
const idle = (v[3] ?? 0) + (v[4] ?? 0)
const total = v.reduce((a, b) => a + b, 0)
return { idle, total }
}
export function cpuPercent(prev: CpuSample, cur: CpuSample): number | undefined {
const dt = cur.total - prev.total
if (dt <= 0) return undefined
return Math.max(0, Math.min(100, (1 - (cur.idle - prev.idle) / dt) * 100))
}
export function parseMem(meminfo: string): { used: number; total: number } | null {
const kb = (k: string) => {
const m = new RegExp(`^${k}:\\s+(\\d+)`, 'm').exec(meminfo)
return m ? Number(m[1]) * 1024 : undefined
}
const total = kb('MemTotal')
const avail = kb('MemAvailable')
if (!total || avail === undefined) return null
return { used: total - avail, total }
}
// df -P <path> 的第二行第五欄
export function parseDf(out: string): number | undefined {
const row = out.trim().split('\n')[1]
const m = row && /(\d+)%/.exec(row)
return m ? Number(m[1]) : undefined
}
+1
View File
@@ -0,0 +1 @@
{ "modules": ["./register.tsx"] }
+114
View File
@@ -0,0 +1,114 @@
import { describe, expect, test } from 'claude-code/testing'
import type { HudData } from './format'
import { bar, day, clock, fit, lineWidth, parseCpu, cpuPercent, parseDf, parseGit, parseMem, parseOffset, prettyModel, shortDir, tokens } from './format'
const RESET_5H = '2026-10-12T07:40:00Z'
const RESET_7D = '2026-10-16T01:00:00Z'
const DATA: HudData = {
model: 'claude-opus-5-5[1m]',
cwd: '/home/dev/work/luopan',
home: '/home/dev',
git: { branch: 'issue-278', dirty: 3, ahead: 1, behind: 0 },
usage: {
fiveHour: { percent: 42, resetsAt: RESET_5H },
sevenDay: { percent: 71, resetsAt: RESET_7D },
ctxTokens: 312_000,
ctxWindow: 1_000_000,
ctxPercent: 31,
},
sys: { cpu: 23, memUsed: 63, memTotal: 100, disk: 78 },
tzOffsetMin: 480,
effort: 'high',
}
const text = (d: HudData, columns: number, compact = false) => fit(d, columns, compact).map(s => s.text).join('')
describe('format', () => {
test('wide line shows every field on one line', async () => {
const line = text(DATA, 200)
expect(line).toBe(
'◆ Opus 5.5 ·high │ ~/work/luopan │ ⎇ issue-278 ±3 ↑1 │ 5h ▰▰▱▱▱ 42% ↻15:40 │ 7d ▰▰▰▰▱ 71% ↻10/16 │ ctx 31% 312k/1.0M │ CPU 23% MEM 63% DSK 78%',
)
expect(line.includes('\n')).toBe(false)
})
test('narrow widths collapse and always fit', async () => {
for (const cols of [140, 120, 100, 90, 80, 60]) {
const segs = fit(DATA, cols)
expect(lineWidth(segs) <= cols || cols < 50).toBe(true)
}
expect(text(DATA, 40)).toBe('◆ Opus ⎇issue-278 5h42 7d71 ctx31 cpu23')
expect(text(DATA, 200, true)).toBe('◆ Opus ⎇issue-278 5h42 7d71 ctx31 cpu23')
})
test('missing figures show a dash', async () => {
const line = text({ ...DATA, git: null, usage: {}, sys: {}, effort: null }, 200)
expect(line).toBe('◆ Opus 5.5 │ ~/work/luopan │ 5h — │ 7d — │ ctx —')
})
test('helpers', async () => {
expect(bar(0)).toBe('▱▱▱▱▱')
expect(bar(100)).toBe('▰▰▰▰▰')
expect(prettyModel('claude-sonnet-5-5')).toBe('Sonnet 5.5')
expect(prettyModel('Opus 5.5')).toBe('Opus 5.5')
expect(tokens(950)).toBe('950')
expect(tokens(200_000)).toBe('200k')
expect(clock(RESET_5H, 480)).toBe('15:40')
expect(day(RESET_7D, 480)).toBe('10/16')
expect(day('2026-10-15T20:00:00Z', 480)).toBe('10/16')
expect(parseOffset('+0800\n')).toBe(480)
expect(parseOffset('-0530')).toBe(-330)
expect(shortDir('/srv/x/y', '/home/dev', true)).toBe('…/y')
expect(shortDir('/home/dev', '/home/dev', false)).toBe('~')
})
test('model tiers', async () => {
const head = (model: string) => fit({ ...DATA, model }, 200)[0]
expect(head('claude-haiku-5-5')).toEqual({ text: '○ Haiku 5.5', color: '#7f9bb3', bold: true })
expect(head('claude-sonnet-5-5')).toEqual({ text: '◇ Sonnet 5.5', color: '#4f9ee8', bold: true })
expect(head('claude-opus-5-5')).toEqual({ text: '◆ Opus 5.5', color: 'claude', bold: true })
expect(head('claude-fable-5-1')).toEqual({ text: '✦ Fable 5.1', color: '#a77bf0', bold: true })
expect(head('some-other-model')).toEqual({ text: '◆ some-other-model', color: undefined, bold: true })
expect(text({ ...DATA, model: 'claude-fable-5-1' }, 40)).toBe('✦ Fable ⎇issue-278 5h42 7d71 ctx31 cpu23')
})
test('parsers', async () => {
expect(parseGit('# branch.oid abc1234ff\n# branch.head main\n# branch.ab +2 -1\n1 .M N... a\n? new.txt\n')).toEqual({
branch: 'main',
dirty: 2,
ahead: 2,
behind: 1,
})
expect(parseGit('# branch.oid abc1234ff\n# branch.head (detached)\n')?.branch).toBe('abc1234')
const a = parseCpu('cpu 100 0 100 700 100 0 0 0 0 0\ncpu0 1')
const b = parseCpu('cpu 150 0 150 750 150 0 0 0 0 0\n')
expect(a && b && cpuPercent(a, b)).toBe(50)
expect(parseMem('MemTotal: 1000 kB\nMemFree: 1 kB\nMemAvailable: 250 kB\n')).toEqual({ used: 768000, total: 1024000 })
expect(parseDf('Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/sdc 100 78 22 78% /\n')).toBe(78)
})
})
describe('band', () => {
test('draws one line above the prompt and hides with /hud off', async ($, on) => {
on('session.model', () => ({ value: 'claude-opus-5-5' }))
on('session.cwd', () => ({ value: '/home/dev/work/luopan' }))
const props = {
hasSurvey: false,
isWorking: false,
maxRows: 10,
bodyColumns: 200,
scroll: { offset: 0, bodyRows: 10 },
view: {},
}
for (const surface of ['terminal', 'desktop'] as const) {
const ui = await $.ui.mount({ plugin: 'hud', surface, component: 'AbovePrompt', props })
expect(await ui.find({ type: 'Text', text: /◆ Opus 5\.5/ })).toBeDefined()
expect(await ui.find({ type: 'Text', text: /5h —/ })).toBeDefined()
await ui.unmount()
}
const ran = await $.command.run({ command: 'hud', args: 'off' } as Parameters<typeof $.command.run>[0])
expect(JSON.stringify(ran)).toContain('隱藏')
})
})
+173
View File
@@ -0,0 +1,173 @@
import { atom, read, update } from 'claude-code'
import type { EngineInterface, Register, SessionContextUsage, SessionRateLimit } from 'claude-code'
import type { Mode, Sys, Usage } from '../types'
import { cpuPercent, fit, parseCpu, parseDf, parseGit, parseMem, parseOffset } from './format'
import type { CpuSample } from './format'
const usageAtom = atom({ plugin: 'hud', key: 'usage' } as const, {})
const gitAtom = atom({ plugin: 'hud', key: 'git' } as const, null)
const sysAtom = atom({ plugin: 'hud', key: 'sys' } as const, {})
const modeAtom = atom({ plugin: 'hud', key: 'mode' } as const, 'auto')
const alertedAtom = atom({ plugin: 'hud', key: 'alerted' } as const, [])
const effortAtom = atom({ plugin: 'hud', 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 天' }
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,
ctxWindow: context.window,
ctxPercent: context.percent,
}
}
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)}%`)
}
}
export const register: Register = on => {
on('session.start', async ($, e, next) => {
await $.command.register({
name: 'hud',
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()
await update($, usageAtom, () => toUsage(u.context, u.rateLimits))
void refreshGit($)
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.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: 'hud' }, async ($, e) => {
const arg = e.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] }
})
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
const mode = await read($, modeAtom)
if (e.props.hasSurvey || mode === 'off') return next(e)
const line = fit(
{
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),
},
e.props.bodyColumns,
mode === 'compact',
)
const { Box, Text } = $.ui.resolve(e)
// 引擎在 band 與輸入框之間已留一行空白,上方補一行讓上下間距相同
return (
<Box marginTop={1}>
<Text wrap="truncate">
{line.map(s => (
<Text color={s.color} dimColor={s.dim} bold={s.bold}>
{s.text}
</Text>
))}
</Text>
</Box>
)
})
}
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "./.claude-plugin/types/tsconfig.json"
}
+28
View File
@@ -0,0 +1,28 @@
export type Window = { percent: number; resetsAt?: string }
export type Usage = {
fiveHour?: Window
sevenDay?: Window
ctxTokens?: number
ctxWindow?: number
ctxPercent?: number
}
export type Git = { branch: string; dirty: number; ahead: number; behind: number }
export type Sys = { cpu?: number; memUsed?: number; memTotal?: number; disk?: number }
export type Mode = 'auto' | 'compact' | 'off'
declare module 'claude-code' {
interface PluginState {
hud: {
usage: Usage
git: Git | null
sys: Sys
mode: Mode
alerted: string[]
effort: string | null
}
}
}