triflux 10.57.2 → 10.58.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/bin/triflux.mjs CHANGED
@@ -3217,6 +3217,14 @@ function resolveUpdateTargets({ installMode, pluginPath }) {
3217
3217
  return [];
3218
3218
  }
3219
3219
 
3220
+ // 키는 "<플러그인>@<마켓플레이스>"다. 접두어로 비교하면 triflux-mods@triflux 를
3221
+ // triflux 본체로 잡아 npm 설치본에서 git pull 을 시도한다.
3222
+ export function findTrifluxPluginEntry(installedPlugins) {
3223
+ for (const [key, entries] of Object.entries(installedPlugins?.plugins || {}))
3224
+ if (key.split("@")[0] === "triflux") return entries[0] ?? null;
3225
+ return null;
3226
+ }
3227
+
3220
3228
  async function cmdUpdate(args = []) {
3221
3229
  if (args.some(isHelpArg)) {
3222
3230
  printCommandHelp("update");
@@ -3233,13 +3241,12 @@ async function cmdUpdate(args = []) {
3233
3241
  // 플러그인 모드 감지
3234
3242
  if (existsSync(pluginsFile)) {
3235
3243
  try {
3236
- const plugins = JSON.parse(readFileSync(pluginsFile, "utf8"));
3237
- for (const [key, entries] of Object.entries(plugins.plugins || {})) {
3238
- if (key.startsWith("triflux")) {
3239
- pluginPath = entries[0]?.installPath;
3240
- installMode = "plugin";
3241
- break;
3242
- }
3244
+ const entry = findTrifluxPluginEntry(
3245
+ JSON.parse(readFileSync(pluginsFile, "utf8")),
3246
+ );
3247
+ if (entry) {
3248
+ pluginPath = entry.installPath;
3249
+ installMode = "plugin";
3243
3250
  }
3244
3251
  } catch {}
3245
3252
  }
@@ -34,7 +34,7 @@ export function resolveClaudeHomeDir(
34
34
  const userProfile = nullableEnv(env.USERPROFILE);
35
35
  return userProfile ? path.resolve(userProfile) : os.homedir();
36
36
  }
37
- // POSIX 는 HOME 우선 — sandbox HOME 오버라이드를 추적하는 provenance
37
+ // POSIX 는 HOME 우선, sandbox HOME 오버라이드를 추적하는 provenance
38
38
  // 해석(#382)과 일치 (os.homedir() 도 POSIX 에서는 HOME 을 우선한다).
39
39
  const home = nullableEnv(env.HOME);
40
40
  return home ? path.resolve(home) : os.homedir();
@@ -223,7 +223,7 @@ export function publicClaudeDaemonCandidate(candidate) {
223
223
 
224
224
  function sessionsFromDaemonList(listResponse) {
225
225
  // probe 경로도 list/find 경로와 같은 정규화 row 를 내보내야 한다
226
- // (jobs/sessions, snake_case, dispatch 중첩 변형 흡수 — 8016b724).
226
+ // (jobs/sessions, snake_case, dispatch 중첩 변형 흡수, 8016b724).
227
227
  return extractClaudeAgentSessions(listResponse);
228
228
  }
229
229
 
@@ -501,7 +501,7 @@ export function sendClaudeControlRequest(
501
501
  // 발급한다. control.sock 은 runtime config hash 를 쓰더라도 auth 는 발급 config
502
502
  // 에서 읽어야 한다. 일반 daemon 은 <configDir>/daemon/control.key 를 사용한다.
503
503
  // 어느 경로에도 파일이 없으면 인증 미강제 daemon 으로 보고 auth 를 생략한다
504
- // (fail-open — 구버전 호환).
504
+ // (fail-open, 구버전 호환).
505
505
  export async function readDaemonControlKey(
506
506
  configDir = resolveClaudeConfigDir(),
507
507
  { diagnostics } = {},
@@ -933,7 +933,7 @@ function isClaudeAttachChromeLine(line) {
933
933
  ) ||
934
934
  /^[-─━▔╭╮╰╯│┌┐└┘├┤┬┴┼\s]+$/.test(trimmed) ||
935
935
  /^(c|x|g):\s/.test(trimmed) ||
936
- /CTX:\d+/.test(trimmed) ||
936
+ /ctx:(?:\d+|--)/i.test(trimmed) ||
937
937
  /auto mode on/.test(trimmed)
938
938
  );
939
939
  }
@@ -1720,7 +1720,7 @@ export async function dispatchClaudeDaemonJob({
1720
1720
  { timeoutMs: dispatchTimeoutMs },
1721
1721
  );
1722
1722
  if (dispatch?.ok !== true) {
1723
- // daemon 거부 사유를 보존한다 — generic 메시지만 던지면 EAUTH 같은
1723
+ // daemon 거부 사유를 보존한다, generic 메시지만 던지면 EAUTH 같은
1724
1724
  // 실원인이 묻혀 진단이 어려워진다.
1725
1725
  const reason = dispatch?.error ? `: ${dispatch.error}` : "";
1726
1726
  throw new Error(
@@ -29,6 +29,7 @@ import {
29
29
  import { buildContextUsageView } from "./context-monitor.mjs";
30
30
  import {
31
31
  fetchClaudeUsage,
32
+ getClaudeEmail,
32
33
  readClaudeUsageSnapshot,
33
34
  scheduleClaudeUsageRefresh,
34
35
  } from "./providers/claude.mjs";
@@ -128,12 +129,10 @@ async function main() {
128
129
  const rows =
129
130
  bandState === "claude"
130
131
  ? []
131
- : getClaudeRows(
132
- currentTier,
133
- contextView,
134
- claudeUsage,
135
- Number(stdin?.cost?.total_cost_usd ?? Number.NaN),
136
- );
132
+ : getClaudeRows(currentTier, contextView, claudeUsage, {
133
+ accountName: getClaudeEmail(),
134
+ promptCache: stdin?.prompt_cache,
135
+ });
137
136
  let codexRowIndex = -1;
138
137
  if (showCodex) {
139
138
  codexRowIndex = rows.length;
@@ -145,6 +144,7 @@ async function main() {
145
144
  codexWhite,
146
145
  codexBuckets ? { type: "codex", buckets: codexBuckets } : null,
147
146
  getCodexEmail(),
147
+ { promptCache: codexSnapshot.promptCache, loggedOut: codexLoggedOut },
148
148
  ),
149
149
  );
150
150
  }
@@ -179,7 +179,7 @@ async function main() {
179
179
 
180
180
  main().catch(() => {
181
181
  process.stdout.write(
182
- `\x1b[0m${bold(claudeOrange("c"))}: ${dim(`5h:--% ${formatTimeCell("")} 1w:--% ${formatTimeCellDH("")} | CTX:--%`)}\n`,
182
+ `\x1b[0m${bold(claudeOrange("c"))}: ${dim(`5h:--% ${formatTimeCell("")} 1w:--% ${formatTimeCellDH("")} | ctx:--%`)}\n`,
183
183
  );
184
184
  });
185
185
 
@@ -0,0 +1,22 @@
1
+ import { dim } from "./colors.mjs";
2
+
3
+ export function getPromptCacheState(expiresAt, ttlMs, now) {
4
+ const remaining = expiresAt - now;
5
+ if (!(remaining > 0)) return "cold";
6
+ return remaining <= Math.min(5 * 60_000, ttlMs * 0.2) ? "cooling" : "warm";
7
+ }
8
+
9
+ // full 게이지와 퍼센트 사이 공백 한 칸을 캐시 표시로 쓴다. 폭은 늘지 않는다.
10
+ export function cacheGap(cache, color, now = Date.now()) {
11
+ const state = getPromptCacheState(cache?.expiresAt, cache?.ttlMs, now);
12
+ if (state === "cold") return " ";
13
+ return state === "cooling" ? dim("+") : color("+");
14
+ }
15
+
16
+ export function claudePromptCache(cache) {
17
+ if (!cache?.warm || cache.caching_observed === false) return null;
18
+ return {
19
+ expiresAt: cache.expires_at * 1000,
20
+ ttlMs: cache.ttl === "1h" ? 60 * 60_000 : 5 * 60_000,
21
+ };
22
+ }
@@ -6,7 +6,8 @@ import { execFileSync, spawn } from "node:child_process";
6
6
  import { createHash } from "node:crypto";
7
7
  import { existsSync, writeFileSync } from "node:fs";
8
8
  import https from "node:https";
9
- import { userInfo } from "node:os";
9
+ import { homedir, userInfo } from "node:os";
10
+ import { join } from "node:path";
10
11
  import {
11
12
  CLAUDE_API_TIMEOUT_MS,
12
13
  CLAUDE_CREDENTIALS_PATH,
@@ -34,6 +35,14 @@ import {
34
35
  writeJsonSafe,
35
36
  } from "../utils.mjs";
36
37
 
38
+ export function getClaudeEmail() {
39
+ const config = readJson(
40
+ join(process.env.CLAUDE_CONFIG_DIR || homedir(), ".claude.json"),
41
+ null,
42
+ );
43
+ return config?.oauthAccount?.emailAddress || null;
44
+ }
45
+
37
46
  export const CLAUDE_USAGE_POLL_BASE_MS = 5_000;
38
47
  export const CLAUDE_USAGE_POLL_JITTER_RATIO = 0.2;
39
48
  const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
@@ -559,7 +568,7 @@ export function readClaudeUsageSnapshot() {
559
568
  return { data, shouldRefresh: schedule.shouldRefresh, isStale: false };
560
569
  }
561
570
 
562
- // 2차: 에러 backoff — 최근 에러 시 재시도 억제 (무한 spawn 방지)
571
+ // 2차: 에러 backoff, 최근 에러 시 재시도 억제 (무한 spawn 방지)
563
572
  if (cache?.error && Number.isFinite(ts)) {
564
573
  if (!schedule.shouldRefresh) {
565
574
  const omcCache = readJson(OMC_PLUGIN_USAGE_CACHE_PATH, null);
@@ -748,7 +757,7 @@ export function scheduleClaudeUsageRefresh() {
748
757
  try {
749
758
  if (!acquireSpawnLock(CLAUDE_REFRESH_LOCK_PATH, SPAWN_LOCK_TTL_MS)) return;
750
759
  } catch {
751
- /* 락 실패 무시 — 스폰 진행 */
760
+ /* 락 실패 무시, 스폰 진행 */
752
761
  }
753
762
 
754
763
  try {
@@ -11,6 +11,7 @@ import {
11
11
  readdirSync,
12
12
  readFileSync,
13
13
  readSync,
14
+ statSync,
14
15
  } from "node:fs";
15
16
  import { join } from "node:path";
16
17
  import {
@@ -130,13 +131,13 @@ function positiveWindowMinutes(bucket) {
130
131
  }
131
132
 
132
133
  // 계정 하나에는 같은 종류의 창이 한 번에 하나만 열린다. Codex 홈(CODEX_HOME)의
133
- // sessions 와 HUD 라벨은 모두 auth.json 계정 하나의 것이다. 그래서
134
- // 세션 로그에 더 늦게 리셋되는 창이 나타났고 옛 창이 그 창이 열리기 전에만 관측됐다면,
135
- // 옛 창은 조기 리셋(2026-09-23 실측)이나 계정 전환으로 끝난 창이다. 프로브로만 본 창은
136
- // 다른 계정일 수 있어 근거로 쓰지 않는다. 새 창이 열린 뒤에도 관측된 창은 함께 도는
137
- // 계정이므로 max-used 후보로 남긴다.
134
+ // sessions, 프로브, HUD 라벨은 모두 auth.json 계정 하나의 것이다. 그래서
135
+ // 더 늦게 리셋되는 창이 나타났고 옛 창이 그 창이 열리기 전에만 관측됐다면,
136
+ // 옛 창은 조기 리셋(2026-09-23, 2026-10-11 실측)이나 계정 전환으로 끝난 창이다.
137
+ // 리셋 뒤 세션을 열기 전에는 새 창이 프로브에만 보이므로 프로브도 근거로 쓴다.
138
+ // 새 창이 열린 뒤에도 관측된 창은 함께 도는 계정이므로 max-used 후보로 남긴다.
138
139
  function isReplacedWindowGroup(group, other) {
139
- if (other === group || !other.seenInSessions) return false;
140
+ if (other === group) return false;
140
141
  if (other.minResetAt <= group.maxResetAt || !other.windowMinutes) {
141
142
  return false;
142
143
  }
@@ -169,14 +170,12 @@ function selectCodexWindowSnapshot(snapshots, nowSec, windowKey) {
169
170
  minResetAt: resetAt,
170
171
  maxResetAt: resetAt,
171
172
  latest: snapshot,
172
- seenInSessions: !snapshot.probe,
173
173
  windowMinutes: positiveWindowMinutes(snapshot[windowKey]),
174
174
  };
175
175
  groups.push(group);
176
176
  continue;
177
177
  }
178
178
 
179
- if (!snapshot.probe) group.seenInSessions = true;
180
179
  group.windowMinutes ??= positiveWindowMinutes(snapshot[windowKey]);
181
180
  group.minResetAt = Math.min(group.minResetAt, resetAt);
182
181
  group.maxResetAt = Math.max(group.maxResetAt, resetAt);
@@ -370,6 +369,90 @@ export function createReverseLineReader(
370
369
  return reader;
371
370
  }
372
371
 
372
+ // GPT-5.6 이후 캐시는 마지막 write/reuse 부터 30분 동안 재사용할 수 있다.
373
+ const CODEX_PROMPT_CACHE_TTL_MS = 30 * 60_000;
374
+
375
+ // 재개한 세션은 생성일 디렉터리의 파일에 계속 쓰므로 날짜가 아니라 수정 시각으로 고른다.
376
+ // TTL 보다 오래 안 바뀐 파일의 캐시는 이미 식었으니 후보에서 뺀다.
377
+ function recentRollouts(sessionsRoot, sinceMs) {
378
+ const found = [];
379
+ const walk = (dir, depth) => {
380
+ let names;
381
+ try {
382
+ names = readdirSync(dir);
383
+ } catch {
384
+ return;
385
+ }
386
+ for (const name of names) {
387
+ const path = join(dir, name);
388
+ if (depth < 3) {
389
+ walk(path, depth + 1);
390
+ continue;
391
+ }
392
+ if (!name.endsWith(".jsonl")) continue;
393
+ try {
394
+ const mtimeMs = statSync(path).mtimeMs;
395
+ if (mtimeMs >= sinceMs) found.push({ path, mtimeMs });
396
+ } catch {
397
+ /* 순회 중 사라진 파일 */
398
+ }
399
+ }
400
+ };
401
+ walk(sessionsRoot, 0);
402
+ return found.sort((a, b) => b.mtimeMs - a.mtimeMs).map((f) => f.path);
403
+ }
404
+
405
+ // 응답마다 한 줄인 token_usage_record 의 마지막 값으로 판정한다. token_count 는 쿼터 갱신 때
406
+ // 이전 사용량을 다시 보내므로 쓰지 않는다. Claude Code 의 prompt_cache.warm 처럼 마지막 응답이
407
+ // 캐시 토큰을 보고했을 때만 살아 있다. 응답 기록이 없으면 undefined.
408
+ function lastResponseCache(path, maxLines = 200) {
409
+ let reader;
410
+ try {
411
+ reader = createReverseLineReader(path);
412
+ } catch {
413
+ return undefined;
414
+ }
415
+ try {
416
+ for (let i = 0; i < maxLines; i++) {
417
+ const line = reader.next();
418
+ if (line === null) return undefined;
419
+ let evt;
420
+ try {
421
+ evt = JSON.parse(line);
422
+ } catch {
423
+ continue;
424
+ }
425
+ const usage =
426
+ evt?.type === "token_usage_record" ? evt.payload?.usage : null;
427
+ if (!usage) continue;
428
+ const cached =
429
+ (usage.cached_input_tokens ?? 0) +
430
+ (usage.cache_write_input_tokens ?? 0);
431
+ const at = Date.parse(evt.timestamp);
432
+ if (cached <= 0 || !Number.isFinite(at)) return null;
433
+ return {
434
+ expiresAt: at + CODEX_PROMPT_CACHE_TTL_MS,
435
+ ttlMs: CODEX_PROMPT_CACHE_TTL_MS,
436
+ };
437
+ }
438
+ return undefined;
439
+ } finally {
440
+ reader.close();
441
+ }
442
+ }
443
+
444
+ // 모델 호출이 없는 부모 세션(하위 세션만 띄운 경우)은 건너뛰고 가장 최근 응답을 쓴다.
445
+ export function readCodexPromptCache(sessionsRoot, now = Date.now()) {
446
+ for (const path of recentRollouts(
447
+ sessionsRoot,
448
+ now - CODEX_PROMPT_CACHE_TTL_MS,
449
+ )) {
450
+ const cache = lastResponseCache(path);
451
+ if (cache !== undefined) return cache;
452
+ }
453
+ return null;
454
+ }
455
+
373
456
  // ============================================================================
374
457
  // Codex 세션 JSONL에서 실제 rate limits 추출
375
458
  // 한계: rate_limits는 세션별 스냅샷이므로 여러 세션 간 토큰 합산은 불가.
@@ -504,13 +587,21 @@ export function getCodexRateLimits({
504
587
  export function readCodexRateLimitSnapshot() {
505
588
  const cache = readJson(CODEX_QUOTA_CACHE_PATH, null);
506
589
  if (!cache?.buckets) {
507
- return { buckets: null, shouldRefresh: true };
590
+ return {
591
+ buckets: null,
592
+ promptCache: cache?.promptCache ?? null,
593
+ shouldRefresh: true,
594
+ };
508
595
  }
509
596
  const buckets = expireStaleCodexBuckets(cache.buckets);
510
597
  const ts = Number(cache.timestamp);
511
598
  const ageMs = Number.isFinite(ts) ? Date.now() - ts : Number.MAX_SAFE_INTEGER;
512
599
  const isFresh = ageMs < CODEX_QUOTA_STALE_MS;
513
- return { buckets, shouldRefresh: !isFresh };
600
+ return {
601
+ buckets,
602
+ promptCache: cache.promptCache ?? null,
603
+ shouldRefresh: !isFresh,
604
+ };
514
605
  }
515
606
 
516
607
  export async function collectProbeSnapshots(now = new Date()) {
@@ -542,12 +633,51 @@ export async function collectProbeSnapshots(now = new Date()) {
542
633
  );
543
634
  }
544
635
 
545
- export async function refreshCodexRateLimitsCache() {
636
+ function getCodexProbeOwner() {
637
+ try {
638
+ const auth = JSON.parse(readFileSync(getCodexAuthPath(), "utf-8"));
639
+ return `${getCodexHome()}|${auth?.tokens?.account_id ?? ""}`;
640
+ } catch {
641
+ return null;
642
+ }
643
+ }
644
+
645
+ // 프로브는 TTL 마다 한 번만 돈다. 그 사이 갱신도 마지막 프로브를 근거로 써야
646
+ // 조기 리셋으로 밀려난 창이 다시 살아나지 않는다. 다른 계정의 결과나, 프로브가
647
+ // 계속 실패해 낡은 결과가 더 최신 세션 로그를 가리지 않도록 이월 범위를 좁힌다.
648
+ function carryProbe(cached, owner, nowMs) {
649
+ if (!owner || cached?.owner !== owner) return null;
650
+ if (!(nowMs - Number(cached.at) < CODEX_PROBE_TTL_MS * 2)) return null;
651
+ return Array.isArray(cached.snapshots) ? cached : null;
652
+ }
653
+
654
+ export async function refreshCodexRateLimitsCache({
655
+ cachePath = CODEX_QUOTA_CACHE_PATH,
656
+ sessionsRoot,
657
+ collect = collectProbeSnapshots,
658
+ owner = getCodexProbeOwner(),
659
+ } = {}) {
546
660
  const now = new Date();
547
- const extraSnapshots = await collectProbeSnapshots(now);
548
- const buckets = getCodexRateLimits({ now, extraSnapshots });
661
+ const probed = await collect(now);
662
+ const probe =
663
+ probed.length > 0
664
+ ? { owner, at: now.getTime(), snapshots: probed }
665
+ : carryProbe(readJson(cachePath, null)?.probe, owner, now.getTime());
666
+ const buckets = getCodexRateLimits({
667
+ sessionsRoot,
668
+ now,
669
+ extraSnapshots: probe?.snapshots || [],
670
+ });
671
+ const promptCache = readCodexPromptCache(
672
+ sessionsRoot || join(getCodexHome(), "sessions"),
673
+ );
549
674
  // buckets가 null이어도 캐시 갱신 (stale 데이터 제거)
550
- writeJsonSafe(CODEX_QUOTA_CACHE_PATH, { timestamp: Date.now(), buckets });
675
+ writeJsonSafe(cachePath, {
676
+ timestamp: Date.now(),
677
+ buckets,
678
+ promptCache,
679
+ probe,
680
+ });
551
681
  return buckets;
552
682
  }
553
683
 
package/hud/renderers.mjs CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  TIME_CELL_INNER_WIDTH,
26
26
  } from "./constants.mjs";
27
27
  import { buildContextUsageView } from "./context-monitor.mjs";
28
+ import { cacheGap, claudePromptCache } from "./prompt-cache.mjs";
28
29
  import { getTerminalColumns, tierBar, tierDimBar } from "./terminal.mjs";
29
30
  import {
30
31
  clampPercent,
@@ -143,7 +144,7 @@ export function getMicroLine(
143
144
  : agyText;
144
145
  segments.push(`${marker}${dim(":")}${agyValue}`);
145
146
  }
146
- segments.push(`${dim("CTX:")}${contextPercentText(ctxView)}`);
147
+ segments.push(`${dim("ctx:")}${contextPercentText(ctxView)}`);
147
148
  return truncateAnsi(segments.join(" "), cols);
148
149
  }
149
150
 
@@ -181,16 +182,16 @@ export function getClaudeRows(
181
182
  currentTier,
182
183
  contextView,
183
184
  claudeUsage,
184
- costUsd = null,
185
+ { accountName, promptCache } = {},
185
186
  ) {
186
187
  const ctxView = contextView || buildContextUsageView({});
187
- const prefix = `${bold(claudeOrange("c"))}:`;
188
- // 이번 세션 비용. 폭이 넉넉한 full, compact 에서만 CTX 뒤에 붙인다.
189
- const costText =
190
- Number.isFinite(costUsd) &&
191
- (currentTier === "full" || currentTier === "compact")
192
- ? ` ${dim(`$${costUsd.toFixed(2)}`)}`
193
- : "";
188
+ const markerColor = claudeOrange;
189
+ const prefix = `${bold(markerColor("c"))}:`;
190
+ // 계정을 ctx 앞에 둬서 x, a 행의 계정 라벨과 같은 열에서 시작하게 한다.
191
+ const accountText = accountName
192
+ ? `${markerColor(fitText(getAccountLabel("claude", accountName), ACCOUNT_LABEL_WIDTH))} `
193
+ : "";
194
+ const contextSection = `${dim("ctx:")}${contextPercentText(ctxView)}`;
194
195
  const spend = spendOnlyPercent(claudeUsage);
195
196
  if (spend != null) {
196
197
  if (currentTier === "nano" || currentTier === "micro") {
@@ -199,7 +200,7 @@ export function getClaudeRows(
199
200
  }
200
201
  const pct = colorByProvider(spend, formatPercentCell(spend), claudeOrange);
201
202
  const left = `${dim("$:")}${tierBar(currentTier, spend, CLAUDE_ORANGE)}${pct}`;
202
- const right = `${dim("CTX:")}${contextPercentText(ctxView)}${costText}`;
203
+ const right = `${accountText}${contextSection}`;
203
204
  return [{ prefix, left, right }];
204
205
  }
205
206
  // API 실측 데이터
@@ -230,10 +231,11 @@ export function getClaudeRows(
230
231
  claudeOrange,
231
232
  )
232
233
  : dim(formatPlaceholderPercentCell());
234
+ const gap = cacheGap(claudePromptCache(promptCache), claudeOrange);
233
235
  const fBar =
234
236
  hasData && fiveHourPercent != null
235
- ? tierBar(currentTier, fiveHourPercent, CLAUDE_ORANGE)
236
- : tierDimBar(currentTier);
237
+ ? tierBar(currentTier, fiveHourPercent, CLAUDE_ORANGE, gap)
238
+ : tierDimBar(currentTier, gap);
237
239
  const wBar =
238
240
  hasData && weeklyPercent != null
239
241
  ? tierBar(currentTier, weeklyPercent, CLAUDE_ORANGE)
@@ -257,7 +259,7 @@ export function getClaudeRows(
257
259
  if (currentTier === "minimal") {
258
260
  const staleTag = claudeUsage?.stale ? ` ${dim("[stale]")}` : "";
259
261
  const quotaSection = `${dim("5h:")}${fStr} ${dim("1w:")}${wStr}${staleTag}`;
260
- const right = `${dim("CTX:")}${contextPercentText(ctxView)}`;
262
+ const right = `${accountText}${contextSection}`;
261
263
  return [{ prefix, left: quotaSection, right }];
262
264
  }
263
265
 
@@ -267,8 +269,13 @@ export function getClaudeRows(
267
269
  const warning = ctxView.warningTag
268
270
  ? ` ${dim("|")} ${yellow(ctxView.warningTag)}`
269
271
  : "";
270
- const contextSection = `${dim("CTX:")}${contextPercentText(ctxView)}${costText}${warning}`;
271
- return [{ prefix, left: quotaSection, right: contextSection }];
272
+ return [
273
+ {
274
+ prefix,
275
+ left: quotaSection,
276
+ right: `${accountText}${contextSection}${warning}`,
277
+ },
278
+ ];
272
279
  }
273
280
 
274
281
  // full tier (>= 120 cols)
@@ -277,8 +284,13 @@ export function getClaudeRows(
277
284
  const warning = ctxView.warningTag
278
285
  ? ` ${dim("|")} ${yellow(ctxView.warningTag)}`
279
286
  : "";
280
- const contextSection = `${dim("CTX:")}${contextPercentText(ctxView)}${costText}${warning}`;
281
- return [{ prefix, left: quotaSection, right: contextSection }];
287
+ return [
288
+ {
289
+ prefix,
290
+ left: quotaSection,
291
+ right: `${accountText}${contextSection}${warning}`,
292
+ },
293
+ ];
282
294
  }
283
295
 
284
296
  // ============================================================================
@@ -296,13 +308,16 @@ export function getProviderRow(
296
308
  markerColor,
297
309
  realQuota,
298
310
  accountName,
311
+ { promptCache, loggedOut = false } = {},
299
312
  ) {
300
313
  const accountLabel = fitText(
301
314
  getAccountLabel(provider, accountName),
302
315
  ACCOUNT_LABEL_WIDTH,
303
316
  );
304
317
 
305
- const prefix = `${bold(markerColor(marker))}:`;
318
+ const prefix = loggedOut
319
+ ? dim(`${marker}:`)
320
+ : `${bold(markerColor(marker))}:`;
306
321
  if (provider === "antigravity") {
307
322
  return getAntigravityRow(
308
323
  currentTier,
@@ -317,7 +332,6 @@ export function getProviderRow(
317
332
  let quotaSection;
318
333
 
319
334
  if (currentTier === "nano" || currentTier === "micro") {
320
- const minPrefix = `${bold(markerColor(`${marker}`))}:`;
321
335
  if (realQuota?.type === "codex") {
322
336
  const main =
323
337
  realQuota.buckets.codex ||
@@ -340,13 +354,13 @@ export function getProviderRow(
340
354
  ? colorByProvider(weekP, `${weekP}%`, provFn)
341
355
  : dim("--%");
342
356
  return {
343
- prefix: minPrefix,
357
+ prefix,
344
358
  left: `${fCellN}${dim("/")}${wCellN}`,
345
359
  right: "",
346
360
  };
347
361
  }
348
362
  }
349
- return { prefix: minPrefix, left: dim("--%/--%"), right: "" };
363
+ return { prefix, left: dim("--%/--%"), right: "" };
350
364
  }
351
365
 
352
366
  if (currentTier === "minimal") {
@@ -424,6 +438,7 @@ export function getProviderRow(
424
438
  }
425
439
 
426
440
  // full tier
441
+ const gap = cacheGap(promptCache, markerColor);
427
442
 
428
443
  if (realQuota?.type === "codex") {
429
444
  const main =
@@ -450,8 +465,8 @@ export function getProviderRow(
450
465
  : dim(formatPlaceholderPercentCell());
451
466
  const fBar =
452
467
  fiveP != null
453
- ? tierBar(currentTier, fiveP, provAnsi)
454
- : tierDimBar(currentTier);
468
+ ? tierBar(currentTier, fiveP, provAnsi, gap)
469
+ : tierDimBar(currentTier, gap);
455
470
  const wBar =
456
471
  weekP != null
457
472
  ? tierBar(currentTier, weekP, provAnsi)
@@ -467,7 +482,7 @@ export function getProviderRow(
467
482
 
468
483
  // 폴백
469
484
  if (!quotaSection) {
470
- quotaSection = `${dim("5h:")}${tierDimBar(currentTier)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell(""))} ${dim("1w:")}${tierDimBar(currentTier)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCellDH(""))}`;
485
+ quotaSection = `${dim("5h:")}${tierDimBar(currentTier, gap)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell(""))} ${dim("1w:")}${tierDimBar(currentTier)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCellDH(""))}`;
471
486
  }
472
487
 
473
488
  const accountSection = `${markerColor(accountLabel)}`;
@@ -515,7 +530,7 @@ function getAntigravityRow(
515
530
  let slot;
516
531
  if (project) {
517
532
  // 쿼터 대신 과금 방식을 막대 자리(막대가 없는 표시에서는 퍼센트 자리)에 보인다.
518
- const mark = isFull ? `${markerColor("GCP".padEnd(GAUGE_WIDTH))} ` : "";
533
+ const mark = isFull ? `${geminiBlue(" GCP ")} ` : "";
519
534
  const cell = isFull ? dim(formatPlaceholderPercentCell()) : percent;
520
535
  slot = `${dim("--:")}${mark}${cell}${timeCell}`;
521
536
  } else {
package/hud/terminal.mjs CHANGED
@@ -136,13 +136,13 @@ export function selectTier() {
136
136
  }
137
137
 
138
138
  // full tier 전용: 게이지 바 접두사 (normal 이하 tier에서는 빈 문자열)
139
- export function tierBar(currentTier, percent, baseColor = null) {
139
+ export function tierBar(currentTier, percent, baseColor = null, gap = " ") {
140
140
  return currentTier === "full"
141
- ? coloredBar(percent, GAUGE_WIDTH, baseColor) + " "
141
+ ? coloredBar(percent, GAUGE_WIDTH, baseColor) + gap
142
142
  : "";
143
143
  }
144
- export function tierDimBar(currentTier) {
144
+ export function tierDimBar(currentTier, gap = " ") {
145
145
  return currentTier === "full"
146
- ? DIM + "░".repeat(GAUGE_WIDTH) + RESET + " "
146
+ ? DIM + "░".repeat(GAUGE_WIDTH) + RESET + gap
147
147
  : "";
148
148
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.57.2",
3
+ "version": "10.58.0",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Antigravity, and Claude",
5
5
  "type": "module",
6
6
  "bin": {