claude-dashboard v1.25~v1.26: 위젯을 쪼개고, 커뮤니티가 테마와 기능을 보태다
claude-dashboard v1.25~v1.26의 변화를 정리합니다. context 위젯을 세 조각으로 쪼개 좁은 터미널에서 필요한 것만 보여주고, tagStatus로 릴리스 태그를 추적하며, 커뮤니티가 peakHours, Pro 7일 한도, Catppuccin Latte 테마를 보태 줬습니다.
1. 지난 이야기
이전 글에서 v1.14~v1.24까지의 변화를 정리했습니다. stdin 데이터 소스 전환, 트랜스크립트 파서 2·3막, lastPrompt 위젯의 history.jsonl 이전 등이 핵심이었고, 별도 딥다이브로 OSC8 하이퍼링크와 negative caching 패턴도 다뤘습니다.
v1.25~v1.26에서는 성격이 조금 달라집니다. 성능이나 데이터 소스보다는 위젯 아키텍처의 진화와 커뮤니티 기여가 주제입니다.
세 줄 요약:
context위젯 하나를 세 개의 서브위젯으로 쪼개서 좁은 터미널에서도 필요한 조각만 골라 쓸 수 있게 했습니다.- git 태그와 HEAD 사이의 거리를 추적하는 tagStatus 위젯을 만들어서 릴리스 상태를 상태줄에서 바로 볼 수 있게 했습니다.
- 커뮤니티에서 peakHours 위젯, Pro 플랜 7일 한도 표시, Catppuccin Latte 라이트 테마를 PR로 보태 줬습니다.
2. 위젯 하나를 세 조각으로 — context 서브위젯 (v1.26.0)
2.1 문제 — context 위젯이 너무 길다
기존 context 위젯은 프로그레스 바, 퍼센트, 토큰 사용량을 한 줄에 다 보여줍니다.
██████░░ │ 80% │ 160K/200K
넓은 터미널에서는 괜찮지만, 분할 화면이나 좁은 창에서는 이것만으로도 한 줄의 절반을 먹습니다. maenwi님이 #63에서 이 문제를 정확히 짚어 주셨습니다. "작은 모니터에서 split 터미널을 쓰면 status line이 잘린다. 바만, 또는 퍼센트만 골라 쓸 수 있으면 좋겠다." 이슈에서 서브위젯 ID 구조와 하위호환 방안까지 함께 제안해 주셨습니다.
2.2 파생 위젯 패턴
이미 sessionId와 sessionIdFull 위젯에서 같은 문제를 풀어 본 적이 있습니다. 같은 데이터를 공유하되, 렌더링만 다른 위젯을 여러 개 등록하는 패턴입니다. 이걸 context에도 적용했습니다.
핵심은 데이터 가져오기 함수를 하나로 뽑아내는 것입니다.
// scripts/widgets/context.ts
// ★ 데이터 로딩 로직은 한 군데에서만 정의
async function getContextData(ctx: WidgetContext): Promise<ContextData | null> {
const { context_window } = ctx.stdin;
const usage = context_window?.current_usage;
const contextSize = context_window?.context_window_size || 200000;
const officialPercent = context_window?.used_percentage;
if (!usage) {
return {
inputTokens: 0, outputTokens: 0, totalTokens: 0,
contextSize,
percentage: typeof officialPercent === 'number' ? Math.round(officialPercent) : 0,
};
}
const inputTokens = usage.input_tokens + usage.cache_creation_input_tokens + usage.cache_read_input_tokens;
const outputTokens = usage.output_tokens;
const totalTokens = inputTokens + outputTokens;
const percentage = typeof officialPercent === 'number'
? Math.round(officialPercent)
: calculatePercent(inputTokens, contextSize);
return { inputTokens, outputTokens, totalTokens, contextSize, percentage };
}
그리고 렌더링 함수도 각각 분리합니다.
function renderBar(data: ContextData): string {
return renderProgressBar(data.percentage);
}
function renderPercentage(data: ContextData): string {
return colorize(`${data.percentage}%`, getColorForPercent(data.percentage));
}
function renderUsage(data: ContextData): string {
return `${formatTokens(data.inputTokens)}/${formatTokens(data.contextSize)}`;
}
이제 위젯 4개가 같은 데이터 함수를 공유하면서 각기 다른 렌더를 쓰게 됩니다.
// 기존 전체 위젯 (하위호환 유지)
export const contextWidget: Widget<ContextData> = {
id: 'context',
name: 'Context',
getData: getContextData,
render(data: ContextData): string {
return [renderBar(data), renderPercentage(data), renderUsage(data)].join(getSeparator());
},
};
// ★ 서브위젯 3종
export const contextBarWidget: Widget<ContextData> = {
id: 'contextBar',
name: 'Context (Bar)',
getData: getContextData,
render: renderBar,
};
export const contextPercentageWidget: Widget<ContextData> = {
id: 'contextPercentage',
name: 'Context (Percentage)',
getData: getContextData,
render: renderPercentage,
};
export const contextUsageWidget: Widget<ContextData> = {
id: 'contextUsage',
name: 'Context (Usage)',
getData: getContextData,
render: renderUsage,
};
2.3 사용 예시
사용자는 preset shorthand로 원하는 조각만 골라 씁니다.
{ "preset": "Mb$R" }
| 프리셋 문자 | 위젯 | 예시 출력 |
|---|---|---|
C |
context (전체) | ██████░░ │ 80% │ 160K/200K |
b |
contextBar | ██████░░ |
% |
contextPercentage | 80% |
# |
contextUsage | 160K/200K |
좁은 터미널에서는 b 하나만 쓰면 프로그레스 바만으로도 컨텍스트 소비량을 직감적으로 알 수 있습니다.
2.4 교훈
파생 위젯 패턴은 "같은 데이터, 다른 프레젠테이션"이라는 조합 폭발을 깔끔하게 관리합니다. getData와 render를 분리한 Widget 인터페이스가 있었기 때문에, 새 서브위젯을 추가할 때 렌더 함수 하나만 매핑하면 끝입니다. 데이터 로딩 로직의 중복은 제로입니다.
3. 릴리스 태그까지 몇 커밋? — tagStatus 위젯 (v1.26.0)
3.1 문제 — 지금 HEAD가 마지막 릴리스에서 얼마나 멀어졌는지 모름
작업하다 보면 **"지금 HEAD가 마지막 릴리스 태그에서 얼마나 앞서 있지?"**가 궁금할 때가 있습니다. git log v1.24.0..HEAD --oneline | wc -l을 수동으로 치면 되지만, 상태줄에 바로 보이면 더 좋겠죠.
3.2 구현 — git describe + rev-list
// scripts/widgets/tag-status.ts
async function resolveTag(
pattern: string,
cwd: string,
): Promise<{ name: string; count: number } | null> {
try {
// 1) 패턴에 매칭되는 가장 가까운 태그 찾기
const described = (
await execGit(
['describe', '--tags', '--abbrev=0', '--match', pattern, 'HEAD'],
cwd, 500,
)
).trim();
if (!described) return null;
// 2) 그 태그와 HEAD 사이의 커밋 수
const countStr = (
await execGit(['rev-list', '--count', `${described}..HEAD`], cwd, 500)
).trim();
const count = parseInt(countStr, 10);
return { name: described, count: Number.isFinite(count) ? count : 0 };
} catch {
return null;
}
}
두 줄의 git 명령이 전부입니다.
git describe --tags --abbrev=0 --match v* HEAD→ 현재 HEAD에서 가장 가까운v*태그 이름git rev-list --count v1.26.0..HEAD→ 그 태그와 HEAD 사이의 커밋 수
3.3 멀티패턴 지원
릴리스 태그(v*)만 추적하는 게 아니라, 내부 sync 마커(handbook-*, blog-extracted 등)도 동시에 추적할 수 있습니다.
{
"tagPatterns": ["v*", "handbook-*"]
}
패턴별로 resolveTag을 Promise.all로 병렬 호출하고, 매칭된 태그만 표시합니다.
const resolved = await Promise.all(patterns.map((p) => resolveTag(p, cwd)));
const tags = resolved.filter((r): r is { name: string; count: number } => r !== null);
렌더링 예시:
🏷 v1.26.0+3 handbook-sync+12
+N은 태그 이후 커밋 수. count === 0이면 접미사를 생략해서 깔끔하게 태그 이름만 보입니다.
3.4 교훈
opt-in 위젯은 opt-out 위젯보다 안전합니다. tagStatus는 기본 프리셋에 포함하지 않았습니다. 태그가 없는 리포에서는 git describe가 실패하고 위젯이 그냥 숨겨지지만, 기본 표시 목록에 넣어 두면 사용자가 "왜 이건 안 나오지?"하고 혼란을 느낄 수 있습니다. 데이터가 있을 때만 의미 있는 위젯은 opt-in이 맞습니다.
4. 피크 타임을 상태줄에 — peakHours 위젯 (v1.25.0, 커뮤니티 기여)
4.1 배경
Anthropic API는 평일 PT 기준 오전 5시~11시에 트래픽이 몰립니다. 이 시간대를 PeakClaude 프로젝트가 추적하고 있었는데, 커뮤니티 컨트리뷰터 OmbraRD님이 이걸 상태줄 위젯으로 만들어 PR을 열어 주셨습니다.
4.2 핵심 — Intl.DateTimeFormat으로 Pacific Time
JavaScript에서 특정 타임존의 현재 시간을 얻는 가장 안정적인 방법은 Intl.DateTimeFormat입니다.
// scripts/widgets/peak-hours.ts
const PACIFIC_FORMATTER = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/Los_Angeles',
hourCycle: 'h23',
hour: 'numeric',
minute: 'numeric',
weekday: 'short',
});
모듈 레벨 상수로 한 번만 생성해 두면, 매 렌더마다 formatToParts(new Date())를 호출해 Pacific Time의 시/분/요일을 얻습니다. TZ 환경변수나 수동 UTC 오프셋 계산 없이 DST까지 자동으로 처리됩니다.
export function isPeakTime(pt: PacificTime): boolean {
return isWeekday(pt.dayOfWeek) && pt.hour >= 5 && pt.hour < 11;
}
렌더링 예시:
Peak (2h12m) ← 지금 피크, 2시간 12분 후 종료
Off-Peak (14h30m) ← 지금 비피크, 14시간 30분 후 시작
4.3 코드 리뷰에서 잡은 것들
PR 리뷰에서 두 가지를 보완했습니다.
-
Intl.DateTimeFormat을 매 호출마다 생성하던 것을 모듈 레벨 상수로 올렸습니다. 이 객체 생성이 의외로 무겁기 때문에(내부적으로 locale negotiation, 타임존 데이터 로딩 등), 상태줄처럼 빈번하게 호출되는 경로에서는 중요합니다. -
getMinutesToTransition을 export해서 테스트에서 직접 호출할 수 있게 했습니다. "금요일 피크 끝나고 다음 피크까지는 몇 분?"같은 주말 건너뛰기 계산을 정확히 검증할 수 있게 됐습니다.
5. Pro 플랜도 7일 한도를 (v1.25.0, 커뮤니티 기여)
기존에는 rateLimit7d 위젯이 Max 플랜 사용자에게만 표시됐습니다. Pro 플랜에는 7일 한도가 없다고 가정했기 때문입니다.
커뮤니티 컨트리뷰터 maenwi님이 이슈를 열어 주셨습니다. Pro 플랜에도 7일 한도가 존재하고, API 응답의 seven_day 필드에 실제 값이 내려온다는 것이었습니다. 수정은 단순했습니다 — plan 조건 분기에서 Pro를 제외하지 않도록 한 줄 바꾸면 끝.
이 PR은 코드 변경보다 **"사용자가 보고한 스펙 오해 수정"**이 핵심이었습니다. 문서와 테스트도 함께 업데이트됐습니다.
6. 라이트 테마와 xhigh — 잔잔한 확장들 (v1.26.0)
6.1 Catppuccin Latte — 첫 라이트 테마
기존 5종 테마(default, minimal, catppuccin, dracula, gruvbox)는 모두 다크 테마였습니다. 커뮤니티 컨트리뷰터 woogii-marc님이 Catppuccin Latte 라이트 테마를 PR로 보내 주셨습니다.
{ "theme": "catppuccinLatte" }
라이트 테마가 들어오면서 "다크만 지원한다"는 무의식적 가정이 하나 깨졌습니다. 기존 테마의 barEmpty 색상이 다크 배경 기준이어서, 밝은 배경에서는 프로그레스 바가 거의 안 보이는 문제가 있었는데 Latte 팔레트에서는 이 부분을 밝은 배경에 맞게 조정했습니다.
6.2 xhigh effort level
Claude Code가 xhigh effort level을 도입하면서, 상태줄에서도 (X) 표기를 추가했습니다.
◆ Opus(X) ← xhigh effort
◆ Opus(H) ← high effort
◆ Opus(M) ↯ ← medium + fast mode
기존의 EffortLevel = 'high' | 'medium' | 'low'에 'xhigh'를 추가하고, 렌더링에서 effortLevel[0].toUpperCase()로 첫 글자만 뽑는 패턴이 그대로 적용됐습니다. 'xhigh'[0]은 'x'이고 toUpperCase()하면 'X'가 되니까 별도 매핑 없이 동작합니다.
동시에, getDefaultEffort 함수에서 모델별로 기본 effort를 추측하던 로직을 **단순 fallback(high)**으로 축소했습니다. Claude Code 본체가 settings.json에 effort를 기록하기 시작했기 때문에, 플러그인이 모델별 기본값을 추측할 이유가 사라졌습니다. stdin > 추측 — v1.21에서 API 호출을 stdin으로 대체한 것과 같은 원칙입니다.
7. 핵심 개념 정리
| 개념 | 적용 위치 | 효과 |
|---|---|---|
| 파생 위젯 패턴 | contextBar/Percentage/Usage |
같은 getData, 다른 render → 조합 폭발 없이 위젯 분할 |
| opt-in 위젯 | tagStatus, peakHours |
데이터 없으면 숨김 — 기본 프리셋에 넣지 않아 노이즈 방지 |
| git describe + rev-list | tagStatus의 resolveTag |
두 줄의 git 명령으로 태그-HEAD 거리 측정 |
| Intl.DateTimeFormat 상수화 | peakHours의 PACIFIC_FORMATTER |
타임존 처리 + DST 자동 + 생성 비용 1회 |
| 상류 데이터 → 추측 제거 | getDefaultEffort 단순화 |
Claude Code가 설정을 기록하면 플러그인이 추측하지 않음 |
| 라이트/다크 테마 분리 | Catppuccin Latte | barEmpty 색상 등 배경 가정을 깨뜨림 |
8. 버전별 변경 요약 (v1.25~v1.26)
| 버전 | 주요 변경 | 기여자 | 카테고리 |
|---|---|---|---|
| v1.25.0 | peakHours 위젯 | OmbraRD님 | 기능 (커뮤니티) |
| v1.25.0 | rateLimit7d Pro 플랜 표시 | maenwi님 | 버그 (커뮤니티) |
| v1.25.0 | fork PR 워크플로우 수정 | — | CI |
| v1.25.1 | release-drafter fork guard | — | CI |
| v1.26.0 | tagStatus 위젯 | — | 기능 |
| v1.26.0 | Catppuccin Latte 테마 | woogii-marc님 | 기능 (커뮤니티) |
| v1.26.0 | xhigh effort level | — | 기능 |
| v1.26.0 | context 서브위젯 분리 | maenwi님 (이슈 제안) | 아키텍처 |
| v1.26.0 | detailed preset 확장 (vimMode, apiDuration, tagStatus) | — | UX |
9. FAQ
Q: context 서브위젯을 쓰면 API 호출이 3배로 늘어나나요?
A: 아닙니다. getData 함수가 동일한 getContextData 하나이고, 같은 렌더 사이클에서는 Widget 시스템이 같은 라인의 위젯들을 Promise.all로 병렬 호출합니다. stdin 데이터를 읽는 것이라 외부 API 호출은 없고, 계산도 한 번만 수행됩니다.
Q: tagPatterns에 아주 많은 패턴을 넣으면 느려지나요?
A: 패턴마다 git describe + git rev-list 두 번의 git 명령이 실행되고 모두 500ms 타임아웃 + Promise.all 병렬입니다. 패턴이 10개면 10쌍이 병렬로 도는데, git 자체는 가벼우므로 실측상 큰 차이가 없습니다. 30초 TTL 캐시도 있어서 이후 렌더에서는 캐시를 씁니다.
Q: peakHours 위젯은 Anthropic 공식 정보인가요?
A: 아닙니다. PeakClaude가 사용자 경험 기반으로 정리한 비공식 정보입니다. Anthropic이 피크 시간대를 공식 발표하진 않았고, 변경될 가능성이 있습니다.
Q: Catppuccin Latte가 기존 다크 테마 코드에 영향을 주나요?
A: 주지 않습니다. 각 테마는 독립된 ThemeColors 객체로, 추가/수정이 다른 테마에 영향을 주지 않습니다. 다만 라이트 테마 도입으로 "barEmpty가 밝은 배경에서도 보여야 한다"는 새로운 요구사항이 생겼고, 이건 Latte 팔레트 내에서 해결됐습니다.
10. 참고 자료
- claude-dashboard GitHub
- PeakClaude — Anthropic API peak hours tracker
- Catppuccin Color Palette — Latte
- MDN: Intl.DateTimeFormat
- git-describe documentation
11. 다음 단계
v1.25~v1.26에서는 "위젯을 쪼개는" 패턴과 "커뮤니티가 기능을 보태는" 흐름이 두 축이었습니다. 앞으로 더 많은 위젯이 서브위젯 분리 후보가 될 수 있고(예: rateLimit의 5h/7d 분리), 커뮤니티 기여도 계속 이어질 것으로 기대합니다.
시리즈 목차:
- Claude Code 상태줄 플러그인 만들기: claude-dashboard 개발기
- Claude Code, Codex, Gemini 사용량을 한 번에 확인하는 CLI 대시보드
- claude-dashboard v1.10~v1.13: 테마, 성능 최적화, 그리고 셸 통합까지
- claude-dashboard v1.14~v1.24: stdin 우선, OSC8 링크, 그리고 파서를 한 번 더 100배 빠르게
- claude-dashboard v1.25~v1.26: 위젯을 쪼개고, 커뮤니티가 테마와 기능을 보태다 ← 현재 글
딥다이브: