triflux 2.0.3 → 2.2.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/README.ko.md +1 -8
- package/README.md +1 -8
- package/hud/hud-qos-status.mjs +224 -82
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -51,14 +51,7 @@
|
|
|
51
51
|
<details>
|
|
52
52
|
<summary><strong>설치</strong></summary>
|
|
53
53
|
|
|
54
|
-
###
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
/plugin marketplace add https://github.com/tellang/triflux
|
|
58
|
-
/plugin install triflux
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### npm (전역)
|
|
54
|
+
### npm (권장)
|
|
62
55
|
|
|
63
56
|
```bash
|
|
64
57
|
npm install -g triflux
|
package/README.md
CHANGED
|
@@ -51,14 +51,7 @@
|
|
|
51
51
|
<details>
|
|
52
52
|
<summary><strong>Install</strong></summary>
|
|
53
53
|
|
|
54
|
-
###
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
/plugin marketplace add https://github.com/tellang/triflux
|
|
58
|
-
/plugin install triflux
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### npm (global)
|
|
54
|
+
### npm (recommended)
|
|
62
55
|
|
|
63
56
|
```bash
|
|
64
57
|
npm install -g triflux
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import https from "node:https";
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { spawn, execSync } from "node:child_process";
|
|
9
9
|
|
|
10
|
-
const VERSION = "1.
|
|
10
|
+
const VERSION = "1.9";
|
|
11
11
|
|
|
12
12
|
// ============================================================================
|
|
13
13
|
// ANSI 색상 (OMC colors.js 스키마 일치)
|
|
@@ -19,7 +19,7 @@ const RED = "\x1b[31m";
|
|
|
19
19
|
const GREEN = "\x1b[32m";
|
|
20
20
|
const YELLOW = "\x1b[33m";
|
|
21
21
|
const CYAN = "\x1b[36m";
|
|
22
|
-
const CLAUDE_ORANGE = "\x1b[38;5;
|
|
22
|
+
const CLAUDE_ORANGE = "\x1b[38;5;173m"; // #D87656 (Claude 테라코타)
|
|
23
23
|
const CODEX_WHITE = "\x1b[97m"; // bright white (SGR 37은 Windows Terminal에서 연회색 매핑)
|
|
24
24
|
const GEMINI_BLUE = "\x1b[38;5;39m";
|
|
25
25
|
|
|
@@ -52,15 +52,50 @@ function colorParallel(current, cap) {
|
|
|
52
52
|
return red(`${current}/${cap}`);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
const GAUGE_WIDTH = 5;
|
|
56
|
+
const GAUGE_BLOCKS = ["░", "▒", "▓", "█"]; // 밝기 0~3
|
|
57
|
+
|
|
58
|
+
function coloredBar(percent, width = GAUGE_WIDTH, baseColor = null) {
|
|
56
59
|
const safePercent = Math.min(100, Math.max(0, percent));
|
|
57
|
-
const
|
|
58
|
-
|
|
60
|
+
const perBlock = 100 / width;
|
|
61
|
+
|
|
62
|
+
// 상태별 색상
|
|
59
63
|
let barColor;
|
|
60
64
|
if (safePercent >= 85) barColor = RED;
|
|
61
65
|
else if (safePercent >= 70) barColor = YELLOW;
|
|
62
|
-
else barColor = GREEN;
|
|
63
|
-
|
|
66
|
+
else barColor = baseColor || GREEN;
|
|
67
|
+
|
|
68
|
+
let bar = "";
|
|
69
|
+
for (let i = 0; i < width; i++) {
|
|
70
|
+
const blockStart = i * perBlock;
|
|
71
|
+
const blockEnd = (i + 1) * perBlock;
|
|
72
|
+
|
|
73
|
+
if (safePercent >= blockEnd) {
|
|
74
|
+
bar += "█"; // 완전 채움
|
|
75
|
+
} else if (safePercent > blockStart) {
|
|
76
|
+
// 프론티어: 구간 내 진행률
|
|
77
|
+
const progress = (safePercent - blockStart) / perBlock;
|
|
78
|
+
if (progress >= 0.75) bar += "▓";
|
|
79
|
+
else if (progress >= 0.33) bar += "▒";
|
|
80
|
+
else bar += "░";
|
|
81
|
+
} else {
|
|
82
|
+
bar += "░"; // 미도달
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 채워진 부분 = barColor, 빈 부분 = DIM
|
|
87
|
+
const filledEnd = Math.ceil(safePercent / perBlock);
|
|
88
|
+
const coloredPart = barColor + bar.slice(0, filledEnd) + RESET;
|
|
89
|
+
const dimPart = filledEnd < width ? DIM + bar.slice(filledEnd) + RESET : "";
|
|
90
|
+
|
|
91
|
+
return coloredPart + dimPart;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 프로바이더별 색상 % (< 70%: 프로바이더 색, ≥ 70%: 경고색)
|
|
95
|
+
function colorByProvider(value, text, providerColorFn) {
|
|
96
|
+
if (value >= 85) return red(text);
|
|
97
|
+
if (value >= 70) return yellow(text);
|
|
98
|
+
return providerColorFn(text);
|
|
64
99
|
}
|
|
65
100
|
|
|
66
101
|
// ============================================================================
|
|
@@ -115,7 +150,7 @@ const GEMINI_SESSION_STALE_MS = 15 * 1000; // 15초
|
|
|
115
150
|
const GEMINI_API_TIMEOUT_MS = 3000; // 3초
|
|
116
151
|
const ACCOUNT_LABEL_WIDTH = 10;
|
|
117
152
|
const PROVIDER_PREFIX_WIDTH = 2;
|
|
118
|
-
const PERCENT_CELL_WIDTH =
|
|
153
|
+
const PERCENT_CELL_WIDTH = 3;
|
|
119
154
|
const TIME_CELL_INNER_WIDTH = 6;
|
|
120
155
|
const CLAUDE_REFRESH_FLAG = "--refresh-claude-usage";
|
|
121
156
|
const CODEX_REFRESH_FLAG = "--refresh-codex-rate-limits";
|
|
@@ -151,6 +186,28 @@ function getTerminalColumns() {
|
|
|
151
186
|
return 0;
|
|
152
187
|
}
|
|
153
188
|
|
|
189
|
+
let _cachedRows = 0;
|
|
190
|
+
function getTerminalRows() {
|
|
191
|
+
if (_cachedRows > 0) return _cachedRows;
|
|
192
|
+
if (process.stdout.rows) { _cachedRows = process.stdout.rows; return _cachedRows; }
|
|
193
|
+
if (process.stderr.rows) { _cachedRows = process.stderr.rows; return _cachedRows; }
|
|
194
|
+
const envLines = Number(process.env.LINES);
|
|
195
|
+
if (envLines > 0) { _cachedRows = envLines; return _cachedRows; }
|
|
196
|
+
try {
|
|
197
|
+
if (process.platform === "win32") {
|
|
198
|
+
const raw = execSync("mode con", { timeout: 2000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true }).toString();
|
|
199
|
+
const m = raw.match(/Lines[^:]*:\s*(\d+)/i) || raw.match(/줄[^:]*:\s*(\d+)/);
|
|
200
|
+
if (m) { _cachedRows = Number(m[1]); return _cachedRows; }
|
|
201
|
+
} else {
|
|
202
|
+
const raw = execSync("tput lines 2>/dev/null || stty size 2>/dev/null | awk '{print $1}'", {
|
|
203
|
+
timeout: 2000, stdio: ["pipe", "pipe", "pipe"],
|
|
204
|
+
}).toString().trim();
|
|
205
|
+
if (raw && !isNaN(Number(raw))) { _cachedRows = Number(raw); return _cachedRows; }
|
|
206
|
+
}
|
|
207
|
+
} catch { /* 감지 실패 */ }
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
154
211
|
function detectCompactMode() {
|
|
155
212
|
// 1. 명시적 CLI 플래그
|
|
156
213
|
if (process.argv.includes("--compact")) return true;
|
|
@@ -191,6 +248,74 @@ function detectMinimalMode() {
|
|
|
191
248
|
|
|
192
249
|
const MINIMAL_MODE = detectMinimalMode();
|
|
193
250
|
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// 4-Tier 적응형 렌더링: full > normal > compact > nano
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// 초기 tier (stdin 없이 결정 가능한 수준)
|
|
255
|
+
let CURRENT_TIER = MINIMAL_MODE ? "nano" : COMPACT_MODE ? "compact" : "normal";
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 인디케이터 인식 + 터미널 크기 기반 tier 자동 선택.
|
|
259
|
+
* main()에서 stdin 수신 후 호출하여 CURRENT_TIER 갱신.
|
|
260
|
+
*/
|
|
261
|
+
function selectTier(stdin, claudeUsage = null) {
|
|
262
|
+
const hudConfig = readJson(HUD_CONFIG_PATH, null);
|
|
263
|
+
|
|
264
|
+
// 1) 명시적 tier 강제 설정
|
|
265
|
+
const forcedTier = hudConfig?.tier;
|
|
266
|
+
if (["full", "normal", "compact", "nano"].includes(forcedTier)) return forcedTier;
|
|
267
|
+
|
|
268
|
+
// 2) 기존 모드 플래그 존중
|
|
269
|
+
if (MINIMAL_MODE) return "nano";
|
|
270
|
+
if (COMPACT_MODE) return "compact";
|
|
271
|
+
|
|
272
|
+
// 3) autoResize 비활성이면 normal 유지
|
|
273
|
+
if (hudConfig?.autoResize === false) return "normal";
|
|
274
|
+
|
|
275
|
+
// 4) 터미널 행/열에서 상태영역 예산 추정
|
|
276
|
+
const rows = getTerminalRows();
|
|
277
|
+
const cols = getTerminalColumns() || 120;
|
|
278
|
+
let budget;
|
|
279
|
+
if (rows >= 40) budget = 6;
|
|
280
|
+
else if (rows >= 35) budget = 5;
|
|
281
|
+
else if (rows >= 28) budget = 4;
|
|
282
|
+
else if (rows >= 22) budget = 3;
|
|
283
|
+
else if (rows > 0) budget = 2;
|
|
284
|
+
else budget = 5; // rows 감지 불가 → 넉넉하게
|
|
285
|
+
|
|
286
|
+
// 5) 인디케이터 줄 추정
|
|
287
|
+
let indicatorRows = 1; // bypass permissions (거의 항상 표시)
|
|
288
|
+
const contextPercent = getContextPercent(stdin);
|
|
289
|
+
if (contextPercent >= 85) indicatorRows += 1; // "Context low" 배너
|
|
290
|
+
// Claude Code 사용량 경고 (노란색 배너: "You've used X% of your ... limit")
|
|
291
|
+
const weeklyPct = claudeUsage?.weeklyPercent ?? 0;
|
|
292
|
+
const fiveHourPct = claudeUsage?.fiveHourPercent ?? 0;
|
|
293
|
+
if (weeklyPct >= 80) indicatorRows += 1;
|
|
294
|
+
if (fiveHourPct >= 80) indicatorRows += 1;
|
|
295
|
+
|
|
296
|
+
// 6) 각 tier에서 줄바꿈 없이 3줄 가용한지 확인
|
|
297
|
+
const tierWidths = { full: 70, normal: 60, compact: 40, nano: 34 };
|
|
298
|
+
for (const tier of ["full", "normal", "compact", "nano"]) {
|
|
299
|
+
const lineWidth = tierWidths[tier];
|
|
300
|
+
const visualRowsPerLine = Math.ceil(lineWidth / Math.max(cols, 1));
|
|
301
|
+
const totalVisualRows = (3 * visualRowsPerLine) + indicatorRows;
|
|
302
|
+
if (totalVisualRows <= budget) return tier;
|
|
303
|
+
}
|
|
304
|
+
return "nano";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// full tier 전용: 게이지 바 접두사 (normal 이하 tier에서는 빈 문자열)
|
|
308
|
+
function tierBar(percent, baseColor = null) {
|
|
309
|
+
return CURRENT_TIER === "full" ? coloredBar(percent, GAUGE_WIDTH, baseColor) + " " : "";
|
|
310
|
+
}
|
|
311
|
+
function tierDimBar() {
|
|
312
|
+
return CURRENT_TIER === "full" ? DIM + "░".repeat(GAUGE_WIDTH) + RESET + " " : "";
|
|
313
|
+
}
|
|
314
|
+
// Gemini ∞% 전용: 무한 쿼터이므로 dim 회색 바
|
|
315
|
+
function tierInfBar() {
|
|
316
|
+
return CURRENT_TIER === "full" ? DIM + "█".repeat(GAUGE_WIDTH) + RESET + " " : "";
|
|
317
|
+
}
|
|
318
|
+
|
|
194
319
|
// ============================================================================
|
|
195
320
|
// 유틸
|
|
196
321
|
// ============================================================================
|
|
@@ -308,22 +433,18 @@ function normalizeTimeToken(value) {
|
|
|
308
433
|
const text = String(value || "n/a");
|
|
309
434
|
const hourMinute = text.match(/^(\d+)h(\d+)m$/);
|
|
310
435
|
if (hourMinute) {
|
|
311
|
-
|
|
312
|
-
const minutes = String(Number(hourMinute[2])).padStart(2, "0");
|
|
313
|
-
return `${hours}h${minutes}m`;
|
|
436
|
+
return `${Number(hourMinute[1])}h${String(Number(hourMinute[2])).padStart(2, "0")}m`;
|
|
314
437
|
}
|
|
315
438
|
const dayHour = text.match(/^(\d+)d(\d+)h$/);
|
|
316
439
|
if (dayHour) {
|
|
317
|
-
|
|
318
|
-
const hours = String(Number(dayHour[2])).padStart(2, "0");
|
|
319
|
-
return `${days}d${hours}h`;
|
|
440
|
+
return `${Number(dayHour[1])}d${Number(dayHour[2])}h`;
|
|
320
441
|
}
|
|
321
442
|
return text;
|
|
322
443
|
}
|
|
323
444
|
|
|
324
445
|
function formatTimeCell(value) {
|
|
325
446
|
const text = normalizeTimeToken(value);
|
|
326
|
-
return `(${text.padStart(TIME_CELL_INNER_WIDTH, "
|
|
447
|
+
return `(${text.padStart(TIME_CELL_INNER_WIDTH, "0")})`;
|
|
327
448
|
}
|
|
328
449
|
|
|
329
450
|
// 주간(d/h) 전용 — 최대 7d00h(5자)이므로 공백 불필요
|
|
@@ -655,11 +776,11 @@ function formatResetRemaining(isoOrUnix) {
|
|
|
655
776
|
const d = typeof isoOrUnix === "string" ? new Date(isoOrUnix) : new Date(isoOrUnix * 1000);
|
|
656
777
|
if (isNaN(d.getTime())) return "";
|
|
657
778
|
const diffMs = d.getTime() - Date.now();
|
|
658
|
-
if (diffMs <= 0) return "
|
|
779
|
+
if (diffMs <= 0) return "0h00m";
|
|
659
780
|
const totalMinutes = Math.floor(diffMs / 60000);
|
|
660
781
|
const totalHours = Math.floor(totalMinutes / 60);
|
|
661
782
|
const minutes = totalMinutes % 60;
|
|
662
|
-
return `${
|
|
783
|
+
return `${totalHours}h${String(minutes).padStart(2, "0")}m`;
|
|
663
784
|
}
|
|
664
785
|
|
|
665
786
|
function isResetPast(isoOrUnix) {
|
|
@@ -673,11 +794,11 @@ function formatResetRemainingDayHour(isoOrUnix) {
|
|
|
673
794
|
const d = typeof isoOrUnix === "string" ? new Date(isoOrUnix) : new Date(isoOrUnix * 1000);
|
|
674
795
|
if (isNaN(d.getTime())) return "";
|
|
675
796
|
const diffMs = d.getTime() - Date.now();
|
|
676
|
-
if (diffMs <= 0) return "
|
|
797
|
+
if (diffMs <= 0) return "0d0h";
|
|
677
798
|
const totalMinutes = Math.floor(diffMs / 60000);
|
|
678
799
|
const days = Math.floor(totalMinutes / (60 * 24));
|
|
679
800
|
const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
|
|
680
|
-
return `${days}d${
|
|
801
|
+
return `${days}d${hours}h`;
|
|
681
802
|
}
|
|
682
803
|
|
|
683
804
|
function calcCooldownLeftSeconds(isoDatetime) {
|
|
@@ -1056,13 +1177,22 @@ function formatSavings(dollars) {
|
|
|
1056
1177
|
return `$${dollars.toFixed(2)}`;
|
|
1057
1178
|
}
|
|
1058
1179
|
|
|
1059
|
-
|
|
1180
|
+
// sv 퍼센트 포맷 (1000+ → k 표기, 5자 고정폭)
|
|
1181
|
+
const SV_CELL_WIDTH = 5;
|
|
1182
|
+
function formatSvPct(value) {
|
|
1183
|
+
if (value == null) return "--%".padStart(SV_CELL_WIDTH);
|
|
1184
|
+
if (value >= 10000) return `${Math.round(value / 1000)}k%`.padStart(SV_CELL_WIDTH);
|
|
1185
|
+
if (value >= 1000) return `${(value / 1000).toFixed(1)}k%`.padStart(SV_CELL_WIDTH);
|
|
1186
|
+
return `${value}%`.padStart(SV_CELL_WIDTH);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
1060
1190
|
const contextPercent = getContextPercent(stdin);
|
|
1061
1191
|
const prefix = `${bold(claudeOrange("c"))}:`;
|
|
1062
1192
|
|
|
1063
|
-
// 절약
|
|
1064
|
-
const
|
|
1065
|
-
const svSuffix = `${dim("sv:")}${
|
|
1193
|
+
// 절약 퍼센트 (Codex+Gemini sv% 합산, x/g와 동일 형식)
|
|
1194
|
+
const svStr = formatSvPct(combinedSvPct || 0);
|
|
1195
|
+
const svSuffix = `${dim("sv:")}${svStr}`;
|
|
1066
1196
|
|
|
1067
1197
|
// API 실측 데이터 사용 (없으면 플레이스홀더)
|
|
1068
1198
|
const fiveHourPercent = claudeUsage?.fiveHourPercent ?? 0;
|
|
@@ -1076,7 +1206,7 @@ function getClaudeRows(stdin, claudeUsage, totalSvDollar) {
|
|
|
1076
1206
|
|
|
1077
1207
|
const hasData = claudeUsage != null;
|
|
1078
1208
|
|
|
1079
|
-
if (
|
|
1209
|
+
if (CURRENT_TIER === "nano") {
|
|
1080
1210
|
const cols = getTerminalColumns() || 80;
|
|
1081
1211
|
if (!hasData) {
|
|
1082
1212
|
const quotaSection = cols < 40
|
|
@@ -1085,38 +1215,37 @@ function getClaudeRows(stdin, claudeUsage, totalSvDollar) {
|
|
|
1085
1215
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1086
1216
|
}
|
|
1087
1217
|
if (cols < 40) {
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
`${colorByPercent(weeklyPercent, `${weeklyPercent}%`)} ` +
|
|
1218
|
+
const quotaSection = `${colorByProvider(fiveHourPercent, `${fiveHourPercent}%`, claudeOrange)}${dim("/")}` +
|
|
1219
|
+
`${colorByProvider(weeklyPercent, `${weeklyPercent}%`, claudeOrange)} ` +
|
|
1091
1220
|
`${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1092
1221
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1093
1222
|
}
|
|
1094
|
-
//
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
-
`${dim("
|
|
1098
|
-
`${svCompact} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1223
|
+
// nano: c: 5h 12% 1w 95% sv: 191% ctx:90%
|
|
1224
|
+
const quotaSection = `${dim("5h")} ${colorByProvider(fiveHourPercent, formatPercentCell(fiveHourPercent), claudeOrange)} ` +
|
|
1225
|
+
`${dim("1w")} ${colorByProvider(weeklyPercent, formatPercentCell(weeklyPercent), claudeOrange)} ` +
|
|
1226
|
+
`${dim("sv:")}${svStr} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1099
1227
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1100
1228
|
}
|
|
1101
1229
|
|
|
1102
|
-
if (
|
|
1103
|
-
// 데이터 없을 때: 전체 dim 처리 (green 0% 대신)
|
|
1230
|
+
if (CURRENT_TIER === "compact") {
|
|
1104
1231
|
if (!hasData) {
|
|
1105
|
-
const quotaSection = `${dim("5h:
|
|
1106
|
-
`${
|
|
1232
|
+
const quotaSection = `${dim("5h:")}${dim(formatPlaceholderPercentCell())} ` +
|
|
1233
|
+
`${dim("1w:")}${dim(formatPlaceholderPercentCell())} ` +
|
|
1234
|
+
`${dim("|")} ${svSuffix} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1107
1235
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1108
1236
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
`${
|
|
1237
|
+
// compact: c: 5h: 14% 1w: 96% | sv: 191% ctx:43%
|
|
1238
|
+
const quotaSection = `${dim("5h:")}${colorByProvider(fiveHourPercent, formatPercentCell(fiveHourPercent), claudeOrange)} ` +
|
|
1239
|
+
`${dim("1w:")}${colorByProvider(weeklyPercent, formatPercentCell(weeklyPercent), claudeOrange)} ` +
|
|
1240
|
+
`${dim("|")} ${svSuffix} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1112
1241
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1113
1242
|
}
|
|
1114
1243
|
|
|
1115
1244
|
// 데이터 없을 때: 퍼센트+시간 모두 dim 처리 (green 0% 대신)
|
|
1116
1245
|
if (!hasData) {
|
|
1117
|
-
const quotaSection = `${dim("5h:")}${dim(formatPlaceholderPercentCell())} ` +
|
|
1246
|
+
const quotaSection = `${dim("5h:")}${tierDimBar()}${dim(formatPlaceholderPercentCell())} ` +
|
|
1118
1247
|
`${dim(formatTimeCell(fiveHourReset))} ` +
|
|
1119
|
-
`${dim("1w:")}${dim(formatPlaceholderPercentCell())} ` +
|
|
1248
|
+
`${dim("1w:")}${tierDimBar()}${dim(formatPlaceholderPercentCell())} ` +
|
|
1120
1249
|
`${dim(formatTimeCellDH(weeklyReset))}`;
|
|
1121
1250
|
const contextSection = `${svSuffix} ${dim("|")} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1122
1251
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
|
@@ -1126,9 +1255,9 @@ function getClaudeRows(stdin, claudeUsage, totalSvDollar) {
|
|
|
1126
1255
|
const weeklyPercentCell = formatPercentCell(weeklyPercent);
|
|
1127
1256
|
const fiveHourTimeCell = formatTimeCell(fiveHourReset);
|
|
1128
1257
|
const weeklyTimeCell = formatTimeCellDH(weeklyReset);
|
|
1129
|
-
const quotaSection = `${dim("5h:")}${
|
|
1258
|
+
const quotaSection = `${dim("5h:")}${tierBar(fiveHourPercent, CLAUDE_ORANGE)}${colorByProvider(fiveHourPercent, fiveHourPercentCell, claudeOrange)} ` +
|
|
1130
1259
|
`${dim(fiveHourTimeCell)} ` +
|
|
1131
|
-
`${dim("1w:")}${
|
|
1260
|
+
`${dim("1w:")}${tierBar(weeklyPercent, CLAUDE_ORANGE)}${colorByProvider(weeklyPercent, weeklyPercentCell, claudeOrange)} ` +
|
|
1132
1261
|
`${dim(weeklyTimeCell)}`;
|
|
1133
1262
|
const contextSection = `${svSuffix} ${dim("|")} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1134
1263
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
|
@@ -1151,26 +1280,30 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1151
1280
|
|
|
1152
1281
|
// 절약 퍼센트 섹션 (context window 대비 %, 4자리 고정폭)
|
|
1153
1282
|
const svPct = savingsMultiplier != null ? Math.round(savingsMultiplier * 100) : null;
|
|
1154
|
-
const svStr = svPct
|
|
1283
|
+
const svStr = formatSvPct(svPct);
|
|
1155
1284
|
const modelLabelStr = modelLabel ? ` ${markerColor(modelLabel)}` : "";
|
|
1156
1285
|
|
|
1286
|
+
// ── 프로바이더별 색상 프로필 ──
|
|
1287
|
+
const provAnsi = provider === "codex" ? CODEX_WHITE : provider === "gemini" ? GEMINI_BLUE : GREEN;
|
|
1288
|
+
const provFn = provider === "codex" ? codexWhite : provider === "gemini" ? geminiBlue : green;
|
|
1289
|
+
|
|
1157
1290
|
// ── 쿼터 섹션 ──
|
|
1158
1291
|
let quotaSection;
|
|
1159
1292
|
let extraRightSection = "";
|
|
1160
1293
|
|
|
1161
|
-
if (
|
|
1294
|
+
if (CURRENT_TIER === "nano") {
|
|
1162
1295
|
const cols = getTerminalColumns() || 80;
|
|
1163
1296
|
const minPrefix = `${bold(markerColor(`${marker}`))}:`;
|
|
1164
|
-
const svCompact = svStr ? ` ${dim("sv:")}${cyan(svStr
|
|
1297
|
+
const svCompact = svStr ? ` ${dim("sv:")}${cyan(svStr)}` : "";
|
|
1165
1298
|
if (realQuota?.type === "codex") {
|
|
1166
1299
|
const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
|
|
1167
1300
|
if (main) {
|
|
1168
1301
|
const fiveP = isResetPast(main.primary?.resets_at) ? 0 : clampPercent(main.primary?.used_percent ?? 0);
|
|
1169
1302
|
const weekP = isResetPast(main.secondary?.resets_at) ? 0 : clampPercent(main.secondary?.used_percent ?? 0);
|
|
1170
1303
|
if (cols < 40) {
|
|
1171
|
-
return { prefix: minPrefix, left: `${
|
|
1304
|
+
return { prefix: minPrefix, left: `${colorByProvider(fiveP, formatPercentCell(fiveP), provFn)}${dim("/")}${colorByProvider(weekP, formatPercentCell(weekP), provFn)}${svCompact}`, right: "" };
|
|
1172
1305
|
}
|
|
1173
|
-
return { prefix: minPrefix, left: `${dim("5h")} ${
|
|
1306
|
+
return { prefix: minPrefix, left: `${dim("5h")} ${colorByProvider(fiveP, formatPercentCell(fiveP), provFn)} ${dim("1w")} ${colorByProvider(weekP, formatPercentCell(weekP), provFn)}${svCompact}`, right: "" };
|
|
1174
1307
|
}
|
|
1175
1308
|
}
|
|
1176
1309
|
if (realQuota?.type === "gemini") {
|
|
@@ -1178,39 +1311,39 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1178
1311
|
if (bucket) {
|
|
1179
1312
|
const usedP = clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
|
|
1180
1313
|
if (cols < 40) {
|
|
1181
|
-
return { prefix: minPrefix, left: `${
|
|
1314
|
+
return { prefix: minPrefix, left: `${colorByProvider(usedP, formatPercentCell(usedP), provFn)}${svCompact}`, right: "" };
|
|
1182
1315
|
}
|
|
1183
|
-
return { prefix: minPrefix, left: `${dim("1d")} ${
|
|
1316
|
+
return { prefix: minPrefix, left: `${dim("1d")} ${colorByProvider(usedP, formatPercentCell(usedP), provFn)} ${dim("1w")} ${dim("\u221E%".padStart(PERCENT_CELL_WIDTH))}${svCompact}`, right: "" };
|
|
1184
1317
|
}
|
|
1185
1318
|
}
|
|
1186
|
-
return { prefix: minPrefix, left: dim("--%"), right: "" };
|
|
1319
|
+
return { prefix: minPrefix, left: dim("--%".padStart(PERCENT_CELL_WIDTH)), right: "" };
|
|
1187
1320
|
}
|
|
1188
1321
|
|
|
1189
|
-
if (
|
|
1190
|
-
// 컴팩트 모드: 바 없이 퍼센트만, right 섹션 생략
|
|
1322
|
+
if (CURRENT_TIER === "compact") {
|
|
1191
1323
|
if (realQuota?.type === "codex") {
|
|
1192
1324
|
const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
|
|
1193
1325
|
if (main) {
|
|
1194
1326
|
const fiveP = isResetPast(main.primary?.resets_at) ? 0 : clampPercent(main.primary?.used_percent ?? 0);
|
|
1195
1327
|
const weekP = isResetPast(main.secondary?.resets_at) ? 0 : clampPercent(main.secondary?.used_percent ?? 0);
|
|
1196
|
-
quotaSection = `${dim("5h:")}${
|
|
1197
|
-
`${dim("1w:")}${
|
|
1328
|
+
quotaSection = `${dim("5h:")}${colorByProvider(fiveP, formatPercentCell(fiveP), provFn)} ` +
|
|
1329
|
+
`${dim("1w:")}${colorByProvider(weekP, formatPercentCell(weekP), provFn)}`;
|
|
1198
1330
|
}
|
|
1199
1331
|
}
|
|
1200
1332
|
if (realQuota?.type === "gemini") {
|
|
1201
1333
|
const bucket = realQuota.quotaBucket;
|
|
1202
1334
|
if (bucket) {
|
|
1203
1335
|
const usedP = clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
|
|
1204
|
-
quotaSection = `${dim("1d:")}${
|
|
1336
|
+
quotaSection = `${dim("1d:")}${colorByProvider(usedP, formatPercentCell(usedP), provFn)} ${dim("1w:")}${dim("\u221E%".padStart(PERCENT_CELL_WIDTH))}`;
|
|
1205
1337
|
} else {
|
|
1206
|
-
quotaSection = `${dim("1d:")}${dim("--%")} ${dim("1w:")}${
|
|
1338
|
+
quotaSection = `${dim("1d:")}${dim("--%".padStart(PERCENT_CELL_WIDTH))} ${dim("1w:")}${dim("\u221E%".padStart(PERCENT_CELL_WIDTH))}`;
|
|
1207
1339
|
}
|
|
1208
1340
|
}
|
|
1209
1341
|
if (!quotaSection) {
|
|
1210
|
-
quotaSection = `${dim("5h:")}${
|
|
1342
|
+
quotaSection = `${dim("5h:")}${provFn("0%".padStart(PERCENT_CELL_WIDTH))} ${dim("1w:")}${provFn("0%".padStart(PERCENT_CELL_WIDTH))}`;
|
|
1211
1343
|
}
|
|
1212
1344
|
const prefix = `${bold(markerColor(`${marker}`))}:`;
|
|
1213
|
-
|
|
1345
|
+
// compact: sv + 계정 (모델 라벨 제거)
|
|
1346
|
+
const compactRight = [svStr ? `${dim("sv:")}${svStr}` : "", accountLabel ? markerColor(accountLabel) : ""].filter(Boolean).join(" ");
|
|
1214
1347
|
return { prefix, left: quotaSection, right: compactRight };
|
|
1215
1348
|
}
|
|
1216
1349
|
|
|
@@ -1221,9 +1354,9 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1221
1354
|
const weekP = isResetPast(main.secondary?.resets_at) ? 0 : clampPercent(main.secondary?.used_percent ?? 0);
|
|
1222
1355
|
const fiveReset = formatResetRemaining(main.primary?.resets_at) || "n/a";
|
|
1223
1356
|
const weekReset = formatResetRemainingDayHour(main.secondary?.resets_at) || "n/a";
|
|
1224
|
-
quotaSection = `${dim("5h:")}${
|
|
1357
|
+
quotaSection = `${dim("5h:")}${tierBar(fiveP, provAnsi)}${colorByProvider(fiveP, formatPercentCell(fiveP), provFn)} ` +
|
|
1225
1358
|
`${dim(formatTimeCell(fiveReset))} ` +
|
|
1226
|
-
`${dim("1w:")}${
|
|
1359
|
+
`${dim("1w:")}${tierBar(weekP, provAnsi)}${colorByProvider(weekP, formatPercentCell(weekP), provFn)} ` +
|
|
1227
1360
|
`${dim(formatTimeCellDH(weekReset))}`;
|
|
1228
1361
|
}
|
|
1229
1362
|
}
|
|
@@ -1233,17 +1366,17 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1233
1366
|
if (bucket) {
|
|
1234
1367
|
const usedP = clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
|
|
1235
1368
|
const rstRemaining = formatResetRemaining(bucket.resetTime) || "n/a";
|
|
1236
|
-
quotaSection = `${dim("1d:")}${
|
|
1237
|
-
`${dim("1w:")}${
|
|
1369
|
+
quotaSection = `${dim("1d:")}${tierBar(usedP, provAnsi)}${colorByProvider(usedP, formatPercentCell(usedP), provFn)} ${dim(formatTimeCell(rstRemaining))} ` +
|
|
1370
|
+
`${dim("1w:")}${tierInfBar()}${dim("\u221E%".padStart(PERCENT_CELL_WIDTH))} ${dim(formatTimeCellDH("-d--h"))}`;
|
|
1238
1371
|
} else {
|
|
1239
|
-
quotaSection = `${dim("1d:")}${dim(formatPlaceholderPercentCell())} ` +
|
|
1240
|
-
`${dim(formatTimeCell("--h--m"))} ${dim("1w:")}${
|
|
1372
|
+
quotaSection = `${dim("1d:")}${tierDimBar()}${dim(formatPlaceholderPercentCell())} ` +
|
|
1373
|
+
`${dim(formatTimeCell("--h--m"))} ${dim("1w:")}${tierInfBar()}${dim("\u221E%".padStart(PERCENT_CELL_WIDTH))} ${dim(formatTimeCellDH("-d--h"))}`;
|
|
1241
1374
|
}
|
|
1242
1375
|
}
|
|
1243
1376
|
|
|
1244
1377
|
// 폴백: 쿼터 데이터 없을 때
|
|
1245
1378
|
if (!quotaSection) {
|
|
1246
|
-
quotaSection = `${dim("5h:")}${dim("--%")} ${dim("1w:")}${dim("--%")}`;
|
|
1379
|
+
quotaSection = `${dim("5h:")}${tierDimBar()}${dim("--%")} ${dim("1w:")}${tierDimBar()}${dim("--%")}`;
|
|
1247
1380
|
}
|
|
1248
1381
|
|
|
1249
1382
|
const prefix = `${bold(markerColor(`${marker}`))}:`;
|
|
@@ -1347,32 +1480,41 @@ async function main() {
|
|
|
1347
1480
|
|| geminiQuota?.buckets?.find((b) => b.modelId === "gemini-3-flash-preview")
|
|
1348
1481
|
|| null;
|
|
1349
1482
|
|
|
1350
|
-
// 합산 절약:
|
|
1351
|
-
const
|
|
1483
|
+
// 합산 절약: Codex+Gemini sv% 합산 (컨텍스트 대비 위임 토큰 비율)
|
|
1484
|
+
const combinedSvPct = Math.round(((codexSv ?? 0) + (geminiSv ?? 0)) * 100);
|
|
1485
|
+
|
|
1486
|
+
// 인디케이터 인식 tier 선택 (stdin + Claude 사용량 기반)
|
|
1487
|
+
CURRENT_TIER = selectTier(stdin, claudeUsageSnapshot.data);
|
|
1488
|
+
|
|
1489
|
+
const codexQuotaData = codexBuckets ? { type: "codex", buckets: codexBuckets } : null;
|
|
1490
|
+
const geminiQuotaData = { type: "gemini", quotaBucket: geminiBucket, session: geminiSession };
|
|
1352
1491
|
|
|
1353
1492
|
const rows = [
|
|
1354
|
-
...getClaudeRows(stdin, claudeUsageSnapshot.data,
|
|
1493
|
+
...getClaudeRows(stdin, claudeUsageSnapshot.data, combinedSvPct),
|
|
1355
1494
|
getProviderRow("codex", "x", codexWhite, qosProfile, accountsConfig, accountsState,
|
|
1356
|
-
|
|
1357
|
-
codexSv, null),
|
|
1495
|
+
codexQuotaData, codexEmail, codexSv, null),
|
|
1358
1496
|
getProviderRow("gemini", "g", geminiBlue, qosProfile, accountsConfig, accountsState,
|
|
1359
|
-
|
|
1360
|
-
geminiSv, getGeminiModelLabel(geminiModel)),
|
|
1497
|
+
geminiQuotaData, geminiEmail, geminiSv, null),
|
|
1361
1498
|
];
|
|
1499
|
+
|
|
1500
|
+
// 비활성 프로바이더 dim 처리: 데이터 없으면 전체 줄 dim
|
|
1501
|
+
const codexActive = codexBuckets != null;
|
|
1502
|
+
const geminiActive = (geminiSession?.total || 0) > 0 || geminiBucket != null;
|
|
1503
|
+
|
|
1362
1504
|
let outputLines = renderAlignedRows(rows);
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
if (
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
outputLines[outputLines.length - 1] += ` ${last.trim()}`;
|
|
1369
|
-
}
|
|
1505
|
+
|
|
1506
|
+
// 비활성 줄 dim 래핑 (rows 순서: [claude, codex, gemini])
|
|
1507
|
+
if (outputLines.length >= 3) {
|
|
1508
|
+
if (!codexActive) outputLines[1] = `${DIM}${stripAnsi(outputLines[1])}${RESET}`;
|
|
1509
|
+
if (!geminiActive) outputLines[2] = `${DIM}${stripAnsi(outputLines[2])}${RESET}`;
|
|
1370
1510
|
}
|
|
1511
|
+
|
|
1371
1512
|
// Context low 메시지 뒤에 HUD가 분리되도록 선행 개행 추가
|
|
1372
1513
|
const contextPercent = getContextPercent(stdin);
|
|
1373
1514
|
const contextLowPrefix = contextPercent >= 85 ? "\n" : "";
|
|
1374
|
-
// RESET
|
|
1375
|
-
|
|
1515
|
+
// 줄별 RESET: Claude Code TUI 스타일 간섭 방지 (색상 밝기 버그 수정)
|
|
1516
|
+
const resetedLines = outputLines.map(line => `\x1b[0m${line}`);
|
|
1517
|
+
process.stdout.write(`${contextLowPrefix}${resetedLines.join("\n")}\n`);
|
|
1376
1518
|
}
|
|
1377
1519
|
|
|
1378
1520
|
main().catch(() => {
|